Welcome to Eiffel language discussion

2009-11-01
2013-05-28
  • Helmut Brandl
    Helmut Brandl
    2009-11-01

    Welcome to Eiffel language discussion

     
  • This is a contribution to the discussion of paper "A solution to the catcall problem" (also of part "Type safety" in "Definition of  'Modern Eiffel'")

    Let us first discuss the question "What is the effect of a catcall?"

    Given the catcall `target.f(arg)', I see the following effects:

    1) Feature call (qualified or unqualified) with the same argument `arg'.  Depending on the actual target and feature two possibilities arise: - another catcall;
    - no catcall since the called feature is not (or not severely) redefined.
    2) Feature call with `arg' as target: the same possibilities as above.
    3) Comparison `a=b' or `a.is_equal(b)'. Two possibilities arise:
    - `a' and `b' are of different type (may be caused by the catcall): the result is `False', i.e. no problem if only the references are compared or if the types are compared first;
    - `a' and `b' are of the same type: no problem since the normal comparison machinery works fine.
    4) Assignment `x := arg': `x' may be get attached to an object of the wrong type.
    5) Assignment attempt `x ?= arg' or `if attached {like x} arg as x_ then x := x_ end': no problem since the actual type of `x' is checked before assignment.

    This means that there may be a recursion of catcalls that stops at a comparison or assignment attempt causing no problems, or at an assignment.  So, even if the list above is not complete assignments have to be discussed in detail. This will be done in the remainder of this contribution.  I will focus on setter routines, since any assignment to attributes has to observe the same validity conditions as setters, and in case of setters the conditions are made explicit by pre- and postconditions.

    Let's suppose that class X1 inherits conforming from class X0 and consider a class containing an attribute and (for demonstration) two setter versions for it.

        class BASE
       
        feature

          x: X0
       
          set_x_1(new_x: like x)
            do
              x := new_x
            ensure
              x_set: x = new_x
            end
       
          set_x_2(new_x: detachable like x)
            do
              if attached {like x} new_x as new then
                x := new
              else
                - What to do here ?
              end
            ensure
              x_set: x = new_x
            end
       
        end
       
    (the second version has been inspired by the proposal in ECMA standard 367 of Eiffel) and a heir class

        class HEIR
       
        inherit
          -> BASE
            redefine x end
       
        feature
          x: X1
       
        end

    As I understand, the proposal of catcall safe type system turns `set_x_*' into
       
          set_x_1(new: X0)
            do
              x := new_x
            end
         
          set_x_2(new_x: detachable X0)
            do
              if attached {like x} new_x as new then
                x := new
              else
                - What to do here ?
              end
            end

    `set_x_1' does not compile: `new_x' does not conform to the redefined `x'!
    `set_x_2' simply goes through the `else' branch if `new_x' is of the wrong type and the routine does nothing, in particular, the postcondition is violated. In any case, the caller fulfilled his part of the contract: he is obligated to provide an X0 object (or even `Void' in case of `set_x_2'), not more, whereas the intention of the programmer of class HEIR (expressed in the postcondition) was to get an argument attached to an X1 object.

    This means that the proposal for safe types is one more proposal to discard conforming redefinition of attributes, we will end up with no-variant redefinition as in C++ or Java. As `set_x_2' shows, the same implications has the proposal in ECMA-Eiffel.

    The situation is comparable to the one described in discussion paper "Improved void safety": the proposal does at best move catcall exceptions to other exceptions (e.g. to postcondition violation). So, the solution should be similar, too:
    - Allow redefinition of attribute types also in case of conforming inheritance, say in the style of classical Eiffel.
    - Insert a runtime check at places where a type mismatch can occur because of a potential catcall (by the discussion above, it is probably sufficient to watch assignments).

    This solution makes it also possible to discard the spurious separation of inheritance into "normal" (meaning non-conforming) and "conforming" ones.  Instead, conforming inheritance gets back its state of being normal (without any special syntactic notation) while non-conforming inheritance becomes again "not-normal" (with special notation, OK, notation `inherit {NONE}' is spurious, too).  Moreover, the old conformance rule for generic types can (and should!) be reestablished.

    Many classes that redefine the type of a query do so by redefining an attribute (or attributes are redefined, too, by anchoring). This means that covariant redefinition of routine arguments will often lead to the problems discussed here. On the other hand, as B.Meyer writes in OOSC, catcalls are rare in practice (this is also my experience), the solution of the catcall problem proposed here (i.e. essentially the same as in classical Eiffel) should be acceptable.

    With regards
    Wolfgang Jansen

     
  • Hello Wolfgang,

    I am afraid I dont' understand your first point. Suppose you have a feature call "t.f(a)" which is a catcall. I.e. t is attached to a descendant object of its static type. The descendant expects in its argument another type of object (a descendant type) as the one to which "a" is attached. This is the condition of a catcall.

    Now there are two possibilities at runtime.

    1. The runtime does not detect the catcall and the redefined routine f in the descendant executes its code as if an object of expected type has been attached to its argument. Then you almost certainly get an inconsistent system (suppose that the expected argument type has more attributes than the provided and the routine attaches something to these in the actual argument object non existing attributes). This inconsistent system will sooner or later crash with the unpleasant effect that at the crash point you will have no clue on what happened before.

    2. The runtime detects the catcall and raises an exception. This exception might be handled in some rescue clause.

    Therefore I cannot see any possiblity that an actual catcall ends in a comparison or an assignment attempt. Execution cannot be resumed smoothely after a catcall. The only possiblity a handled exception with a successful retry.

    As to your example: Both setters compile safely within modern Eiffel as long as you specify "class HEIR inherit BASE redefine x end … end". "Normal" inheritance (I am not yet happy with the name) does allow redefinition of attributes and redefinition of arguments as long as they are anchored. Under conforming inheritance the redefinition is not allowed!

    I often hear the argument that catcalls are rare in practice. I agree. If you are an experienced programmer I claim that even type errors are very rare in practice. E.g. we could kickout the type checker from the compiler and do type checking at runtime. So if you have an assignment "x:=exp" and the object returned by "exp" does not conform to the type of "x" a runtime exception occurs. I would claim that such runtime errors occur very rarely in practice. But should we kickout the type checker from the compiler? Certainly no. The proposal for type safe Eiffel just makes the type checker waterproof such that catcalls (which is a type error) cannot occur at runtime and are detected by the compiler.

     
  • Hi Helmut,

    sorry, for being unclear. I am interested in conforming inheritance (this should have become clear by `inherit -> BASE' in class HEIR of the example).

    The first part of my yesterday's contribution concerns your item 1. If the runtime system does not detect the catcall computation continues and will execute the instructions of the - erroneously called - routine. My contribution concerns what happens in this routine: either the catcall is propagated as another catcall to another routine call or it is not. If it is propagated then we are in the same situation as now (what I called "recursion of the catcall" because type mismatches continue), if it is not propagated (what I called "recursion of catcalls stops") then it has dangerous effects at the instruction in question, or it has not. So far, I see that only assignments are dangerous.
    Moreover, I supposed implicitly that, in case of propagation by calling a routine on a target that is an argument of the current routine, the polymorphic dispatch will work correctly. I overlooked this supposition yesterday.

    This is the contents of the first part. The second part should demonstrate that assignments are actually dangerous, and that runtime checks are not avoidable here and only here.

    You write that redefinition of attributes is forbidden in Modern Eiffel. May be that I have something missed when reading your discussion paper. What I read was:

    "3. Conforming inheritance (inherit ->) does not allow covariant redefinitions of arguments. In case of class C inherit -> B … end all anchored arguments of features of B are deanchored within B before being inherited in C."
    Attributes are mentioned only in connection with generic types.

    If not merely arguments but also attributes cannot be redefined then this is a terrible decision. I am convinced that conforming inheritance should be the "normal" one. And discarding an important language feature of classical Eiffel (a feature where Eiffel goes far beyond what other prominent OO languages offer) from normal language use cannot be accepted.

    I cannot judge whether I am an experienced programmer, I just have done some programming in Eiffel for a dozen years. You are right, type errors (other than in connection with catcalls) are rare, they occur when I am absent minded for a moment, and they are easily fixed when think about the entity types a second time. Catcalls are rare, too, but when they occur then it is much harder to fix them. Anyway, we need a type checker in the compiler (at least for absent minded people), and I am happy that there is one. My two cents merely state that the type checker should insert runtime checks at places where it neither can ensure nor disprove type correctness, that this is necessary at less places than one might fear, and (first of all) that the language should not be pruned so much.

     
  • Helmut Brandl
    Helmut Brandl
    2011-10-14

    Ok, now I have got your point. You consider the case that a catcall is not detected by the runtime and execution continues.

    Then of course assignments are problematic. Or lets state it more generally: Commands are problematic, because finally commands modify something and a modification must end up in some assignment (This is the only primitive modifying command). And if you assign to a location which is not existent, the consequences have chances to be catastrophic (you overwrite memory in locations which might not belong to the object). Queries are not that catastrophic, because the just read "unauthorized" in memory locations which do not belong to the object. But anyhow, the consequence will be spurious and unexpected and difficult to trace. And the runtime has usually no chance to detect the error.

    Therefore: If the compiler does not detect catcalls at compile time, the runtime has to detect them and raise an exception. If it does not do that, the results are unpredictable.

    Discussion on Modern Eiffel:

    Modern Eiffel forbids redefinition of arguments only for conforming inheritance. Redefinition of queries (and attributes) is no problem under conforming inheritance.

    The other types of inheritance allow covariant redefinition of arguments. "Normal" inheritance (I don't like the word either, but I have not yet come up with a better one) allows covariant redefintions of arguments as long as they are anchored. So if you anchor an argument to an attribute and redefine the attribute, the argument is redefined as well. This is a powerful feature which should not be dropped.

    So you have the decision: Inherit conforming, then you cannot use covariant redefinition or inherit private of normal you can redefine attributes. Think of specific examples. In my practice it has always been clear if I want conforming inheritance (i.e. type with runtime polymorphy) or redefinition of arguments. I have not yet encountered case where I want to use both at the same time. And if yes, it is always possible to refactor the base class into two classes, one containing the "polymorphic" features and the other containing the features with argument redefinitions (e.g. in Modern Eiffel ANY has been factored in ANY (no anchored arguments) and OBJECT (with anchored arguments, e.g. is_equal)).

    For me the definition is still work in progress and subject to change. I am currently rewriting a lot of libraries to see whether this distinction of inheritance type can be integrated naturally or weather it causes ugly workarounds. If it integrates naturally, then this distinction will be probably kept, because type safety is good feature. If not, the runtime checks are inevitable.

    Look at this in that way: Conforming inheritance offers all possibilities which other type safe OO languages (C#, scala) have as well. They all have no possibility to redefine arguments covariantly (for good reasons). In addition to that Modern Eiffel offers you to covariantly redefine arguments, but you cannot combine this with runtime polymorphy.

     
  • I understand your intention but I am not a friend of this language design. In other words, what prevents me and others to switch to Scala? (OK, it would be much reprogramming work).

    To give an example scheme where conforming inheritance and redefinition of attributes (and their setters!) naturally occur together, consider the following.
    In my project there are several instances of "parallel" inheritance:

      A0   ==>  B0 . . .
        |              |
        v             v
       A1   ==>  B1 . . .
        .         .
        .         .
        .         .

    meaning that the A* classes have attributes of the corresponding B* classes and *1 classes inherit from *0. In general, there is a 1..n relation between A* and B*, i.e. it is not possible to pack A* and the corresponding B* into one class. Both, the A* line and the B* line, add some features, redefine some routines and, of course, the A* line redefines attributes related to B*.  Moreover, there is a client class whose objects are initially fed with an A* object (which one is decided at runtime).  Statically, the client class works always on A0 and B0 objects. To work actually on A1 and B1 objects we need polymorphic dispatch. In terms of Modern Eiffel conforming inheritance is to be chosen. Within A1 it is advantageous to know that the B* attributes (more generally, queries of B* type) are actually of type B1, this way, it is not necessary to write (and execute!)
        if attached {B1} b as b1 then b1.do_some_thing end
    here and there. That means, it is advantageous that attributes can be redefined and their setters, too. Actually, I did this in many classes and I am interested not to loose this class design.  If the price is the possibility of catcalls then I will accept catcalls (because they are rare in practice) provided that they are caught at runtime.

    To be more precise, think of classes LINKED_LIST, TWO_WAY_LIST, LINKABLE, and BI_LINKABLE for A0, A1, B0, B1, respectively.  Class BI_LINKABLE adds attribute `left' and several routines working on `left'.  How can these classes be written in Modern Eiffel such that a TWO_WAY_LIST object can be used in lieu of a LINKED_LIST object?

     
  • Addition to the previous mail.
    Actually the inheritance and client scheme is as follows:

            A0   ==>    B0     . . .
          /    \       /   \
         |      |     |     |
         |      v     |     v
         |      B2  = | =>  B2 . . .
         |            |
         v            v
         A1    ==>    B1       . . .
         .            .
         .            .
         .            .

    where A0, B0 are deferred, A1, B1 and A2, B2 are parallel effective descendants (no inheritance or client relation between *1 and *2 classes). The choice is between objects of *1 and *2 types. Catcalls do not occur since always A1 or A2 objects are combined with B1 or B2 objects only. I use the GOBO compiler for compilation that maintains the dynamic typeset of each expression. Since typesets for *1 entities are always disjoint from typesets of *2 entities, the compiler does correctly not issue catcall errors. Things are different in the scheme of the previous mail. Since *1 objects are assigned to *0 entities in the supervising client, the typesets of *0 some entities contain both, *0 and *1, types and a (possible) catcall is detected (and, depending on compiler switches, the system may be rejected).

    Would this more developed design scheme be acceptable for a revised Modern Eiffel?

     
  • Helmut Brandl
    Helmut Brandl
    2011-10-22

    Sorry, that it took so long to respond, but I had been off from the internet for a week now.

    Your design is very interesting and want Modern Eiffel to be capable to express this design in a typesafe manner. As far as I understand your design pattern is called "familiy polymorphism". There has been some research in this topic. In order to express this pattern in a type safe manner we need local types. I have updated the Modern Eiffel description. Look into the description and we should discuss your design in the light of that pattern.

    By the way. Can you send me your design or a downsized version of your design in order to check if it can be done with local types?

    In scala you can express your design only using "path dependent types" (the equivalent of local types). Scala is typesafe and does not allow covariant redefinitions of arguments. But you can use path dependent types.

    Regards
    Helmut