Wednesday, March 14, 2012

Identifying "new" vs. "overriding" operations when extending a module

We have recently implemented inheritance of operations and components using the extends keyword in ParaSail.  One of the frequent problems with inheritance is that the programmer might think they are or are not overriding an inherited operation when they explicitly declare an operation, but in fact they are wrong.

Object-oriented languages have addressed the issue of overriding "intent" in various ways.  In C#, a method must have the word "override" if it overrides (as opposed to hides), and may have the word "new" if it doesn't.  In Java 5+, there is an "@Override" annotation.  In Eiffel, "redefine" may be used to indicate overriding.  In Ada 2005, "overriding" or "not overriding" may be indicated.  In C++11, the (non-reserved) keyword "override" may be used to indicate the intent to override.

Our initial design for ParaSail lacked any way to indicate overriding intent, but given the fact that essentially all mainstream object-oriented programming languages now have that ability, it seemed like an oversight.  Therefore, we now allow an interface that extends another to separate the overridings from the new operations.  Here is an example of an Expr interface, and an interface Binary that extends it:

interface Expr<> is
    type Type_Enum is Enum<[#bool, #int, #real, #array, #record];>;

    abstract func Eval(E : Expr) -> Univ_Real;
      // Operation to evaluate expression tree to a univ-real result
    abstract func Display(E : Expr; Indent : Univ_Integer := 0);
      // Operation to display an expression tree in an indented format

    func Init_Type(T : Type_Enum; Count : Univ_Integer) -> Expr;
      // Create an initialized Expr object as needed for
      // creating a class-aggregate in any descendant of Expr.
      // Such a constructor is needed because Expr has hidden
      // components (the "Type" component is hidden).
end interface Expr; 

interface Binary<> extends Expr is
    type Binop is Enum<[#plus, #minus, #times, #divide, #pow]>;

    // Override abstract "Expr" operations
    func Eval(B : Binary) -> Univ_Real;
    func Display(B : Binary; Indent : Univ_Integer := 0);

    // Override inherited constructor, which "becomes" abstract
    // because Binary has its own constructor, and hence might have
    // its own private components.
    func Init_Type(T : Expr::Type_Enum; Count : Univ_Integer) -> Binary; 
    // Define constructor for Binary nodes
    func Create(Op : Binop; Left, Right : Expr+) -> Binary;
end interface Binary;

The reserved word new, if present, separates the declarations which are overriding inherited operations, from the declarations which correspond to new operations.  The compiler will complain if declarations in the first set do not override an inherited operation, or declarations in the second set do override an inherited operation.

One open design question at the moment is whether an operation should be declared before or after new if it implements an operation declared in an interface named in the implements list, but does not override an operation inherited from the parent interface named after extends.  It is not actually overriding anything that was inherited, but it is not really new since it is presumably intended to match an existing operation in some implemented interface. 

My sense is that we will catch more errors by requiring that an operation that comes after new is really new, and does not override an inherited operation nor does it implement an operation of an implemented interface.  And for those who like to think that operations are effectively inherited from implemented (as opposed to extended) interfaces, then distinguishing the two kinds of inheritance (interface inheritance vs. implementation inheritance) might be confusing. 

Note that one issue is that ParaSail allows an interface to implement another parameterless interface without mentioning it explicitly in the implements list.  This provides a kind of ad hoc matching and reduces the number of interfaces that need to be mentioned explicitly.  This kind of ad hoc matching is clearly not considered when checking the proper placement of the new separator.

No comments:

Post a Comment