master branch, virtual methods

Posted on Sun Jul 24, 2016

There! I merged the ‘typed-language’ branch into the master branch of Vicare. I have also rotated the boot images, so it is now easier to catch errors in the source code of the boot image.

Some work has gone into methods. With simple inheritance, overloaded functions, methods, overloaded methods, virtual methods, sealing methods, interfaces, mixins, the toolbox for object–oriented programming is almost complete. Still missing are recursive types (indispensable) and maybe public/private members for record–types.

Everything I discuss here is relative to code in the head of the master branch.

Method functions

An implementation of virtual methods for record–types is in the code; sealing methods are also there. Let’s just do a recapitulation of methods by a good copy and paste from the documentation, let’s go…

Methods are possibly overloaded typed functions whose syntactic binding resides in a namespace reserved to an object–type and that are accessible only through an instance of such object–type.

The syntactic bindings method, virtual-method and seal-method are used as clause keywords in uses of define-record-type to define methods and virtual methods for a record–type; they can be used any number of times in the same define-record-type macro use.

The clause method is also accepted by define-label-type, define-interface-type and define-mixin-type.

The clauses virtual-method and seal-method are also accepted by define-mixin-type.

Concrete methods

We can think of method as acting like define/typed with regard to the syntax of arguments to function; the first argument to a method is the record itself, but it is implicit: we can access it using the fluid syntax this. For example, using the procedural coding style:

(define-record-type <duo>
  (strip-angular-parentheses)
  (fields one two)
  (method (sum-them)
    (+ (duo-one this)
       (duo-two this)))
  (method (mul-them)
    (* (duo-one this)
       (duo-two this))))

(define O
  (make-duo 1 2))

(method-call sum-them O)        ⇒ 3

and using the object–oriented syntax style:

(define-record-type <duo>
  (fields one two)
  (method (sum-them)
    (+ (.one this)
       (.two this)))
  (method (mul-them)
    (* (.one this)
       (.two this))))

(define O
  (new <duo> 1 2))

(.sum-them O)   ⇒ 3

The syntax method-call searches for a record–type’s methods by using eq? to search for the method’s name, as symbol, in the record–type’s internal table of methods (it does not use the syntactic identifiers with free-identifier=?).

Overloaded methods

The clause method allows the definition of overloaded functions, with multiple function specialisations associated to the same method name. Example:

(define-record-type <alpha>
  (fields a b)

  (method ({doit <list>} {A <fixnum>})
    (list (.a this) (.b this) 'fixnum A))

  (method ({doit <list>} {A <symbol>})
    (list (.a this) (.b this) 'symbol A))

  (method ({doit <list>} {A <number>} {B <number>})
    (list (.a this) (.b this) 'numbers A B)))

(define O
  (new <alpha> 1 2))

(.doit O 123)           ⇒ (1 2 fixnum 123)
(.doit O 'ciao)         ⇒ (1 2 symbol ciao)
(.doit O 3 4)           ⇒ (1 2 numbers 3 4)

we can think of the methods defined above as expanding to the following definitions:

(define/overload ({doit <list>} {subject <alpha>}
                      {A <fixnum>})
  (fluid-let-syntax ((this (identifier-syntax subject)))
    (list (.a this) (.b this) 'fixnum A)))

(define/overload ({doit <list>} {subject <alpha>}
                      {A <symbol>})
  (fluid-let-syntax ((this (identifier-syntax subject)))
    (list (.a this) (.b this) 'symbol A)))

(define/overload ({doit <list>} {subject <alpha>}
                      {A <number>} {B <number>})
  (fluid-let-syntax ((this (identifier-syntax subject)))
    (list (.a this) (.b this) 'numbers A B)))

where subject is a non–accessible identifier.

Field methods

When methods are defined for record-types: method names cannot be equal to field names; field accessors and mutators are accessible as methods automatically, with a method name equal to the field name. The syntax of method-call is:

(method-call ?name ?subject-expr ?arg ...)

when there are no ?arg operand the syntax is compatible with a field accessor call; when there is a single ?arg operand the syntax is compatible with a field mutator call.

So we can access and mutate fields as follows:

(define-record-type <alpha>
  (fields (mutable a)))

(define O
  (new <alpha> 1))

(.a O)          ⇒ 1
(.a O 2)
(.a O)          ⇒ 2

Overriding methods

When a hierarchy of record–types is defined: the sub–types can override the super–type’s concrete methods by defining methods with the same name. Example:

(define-record-type <super>
  (method (doit)
    1))

(define-record-type <sub>
  (parent <super>)
  (method (doit)
    2))

(define (super-fun {O <super>})
  (.doit O))

(define (sub-fun   {O <sub>})
  (.doit O))

(define O
  (new <sub>))

(super-fun O)   ⇒ 1
(sub-fun   O)   ⇒ 2

The method of the super–type is still accessible if we “see” the object instance through the super–type’s type specification.

The sub–type’s method is in no way limited by the super–type’s method: the two methods are allowed to have completely different type signature.

Virtual methods

The clause virtual-method allows the definition of virtual methods associated to a record–type. Virtual methods work almost like concrete methods, but the rules of overriding are different: with virtual methods we request run–time dynamic dispatching, to have the methods of the sub–types take precedence over the methods of the super–types.

NOTE Virtual methods must have an implementation, there are no abstract methods.

Example:

(define-record-type <super>
  (virtual-method (doit)
    1))

(define-record-type <sub>
  (parent <super>)
  (method (doit)
    2))

(define (super-fun {O <super>})
  (.doit O))

(define (sub-fun   {O <sub>})
  (.doit O))

(define O
  (new <sub>))

(super-fun O)   ⇒ 2
(sub-fun   O)   ⇒ 2

we see that even though the function super-fun accesses the instance of <sub> with the type specification <super>, the actually called method is the one defined by <sub>.

The overriding method must have type signature being a sub–type of the overridden type signature. For example, the following definitions are fine:

(define-record-type <super>
  (fields value)
  (virtual-method ({doit <number>} {S <nestring>})
    (.value this)))

(define-record-type <sub>
  (parent <super>)
  (virtual-method ({doit <fixnum>} {S <string>})
    (.value this)))

because:

(type-annotation-super-and-sub?
   (lambda (<nestring>) => (<number>))
   (lambda (<string>)   => (<fixnum>)))
⇒ #t

while the following definitions will cause a syntax violation exception at expand–time:

(define-record-type <super>
  (fields value)
  (virtual-method ({doit <number>} {S <nestring>})
    (.value this)))

(define-record-type <sub>
  (parent <super>)
  (virtual-method ({doit <fixnum>} {S <vector>})
    (.value this)))

because:

(type-annotation-super-and-sub?
   (lambda (<nestring>) => (<number>))
   (lambda (<string>)   => (<vector>)))
⇒ #f

Once a method has been defined as virtual in a super–type, it does not matter if the sub–type makes use of method, virtual-method or seal-method: all these clauses will override the super–type’s method if they use the same name. Example:

(define-record-type <super>
  (virtual-method (darkness)
    1)
  (virtual-method (light)
    2))

(define-record-type <sub>
  (parent <super>)
  (method (darkness)
    11)
  (virtual-method (light)
    22))

(define O
  (new <sub>))

(define {P <super>}
  O)

(.darkness P)   ⇒ 11
(.light    P)   ⇒ 22

Notice that it is not possible to define a sub–type having a field with the same name of a super–type’s virtual method; the following definitions will cause an expand–time exception:

(define-record-type <super>
  (method (doit)
    1))

(define-record-type <sub>
  (parent <super>)
  (fields doit))
error→ &syntax

The clause seal-method seals a method name so that the sub–types can no more use it; it does not matter if the super–types actually use it or not.

Sealing methods

It happens that we want to define virtual methods in a super–type, override them in a sub–type and then forbid further overriding in the sub–types of the sub-type. This is possible with sealing methods.

The clause seal-method allows the definition of methods associated to a record–type that might override the super–type’s methods, but that forbid the sub–types from overriding them. For everything else: sealing methods work like concrete methods.

In the following example everything works as usual, with <sub> overriding the implementation of doit in <super>:

(define-record-type <super>
  (virtual-method (doit)
    1))

(define-record-type <sub>
  (parent <super>)
  (seal-method (doit)
    2))

(define (super-fun {O <super>})
  (.doit O))

(define (sub-fun   {O <sub>})
  (.doit O))

(define O
  (new <sub>))

(super-fun O)   ⇒ 2
(sub-fun   O)   ⇒ 2

but adding the following definition will cause an expand–time exception, because sub–types of <sub> are forbidden from having a method named doit:

(define-record-type <sub-sub>
  (parent <sub>)
  (method (doit)
    3))
error→ &syntax

the following definition will also cause an expand–time exception, because sub–types of <sub> are forbidden from having a field named doit:

(define-record-type <sub-sub>
  (parent <sub>)
  (fields doit))
error→ &syntax