Next: , Previous: , Up: 2016   [Contents][Index]


hash, methods overhaul, interfaces again

Posted on Wed Jul 13, 2016

More work in the ‘typed-languagebranch of Vicare, for both the expander and the built–in types infrastructure.

Everything I discuss here is relative to code in the head of the ‘typed-language’ branch.

Syntax hash

Every object–type in Vicare provides a hash function; the core object–types have a default hash function, while record–types have a customisable hash function. A hash value can be computed with the new syntax hash, with the use template:

(hash ?expr)

If the expander can determine the type of the value returned by ?expr: the syntax use is expanded into an application of the appropriate hash function. Otherwise, the type of the returned value is determined at run–time and the appropriate hash function retrieved and applied. If ?expr does not return a single value: an exception is raised.

Some syntaxes renamed

For coherence with define-record-type: the syntax define-label has been renamed to define-label-type; the syntax define-interface has been renamed to define-interface-type; the syntax define-mixin has been renamed to define-mixin-type. Strictly speaking: mixins are not a type, but still that is the syntax name.

Methods overhauling

Up to some commits ago, all the syntaxes supporting method specifications (like define-record-type) accepted the clauses method, case-method and method/overload to define methods. I changed this so that the only clause is now method.

The clause case-method defined a typed function with case-define, having multiple lambda clauses; it is clear that the same result can be achieved with an overloaded method having a set of specialisations. So the case-method has been removed.

Whenever a method with a specific name is defined only once: the new method clause will define a normal method implementation function, using define.

Whenever a method with a specific name is defined multiple times: the new method clause will act like the old method/overload clause, defining an overloaded function with define/overload.

Interface types

The previous, experimental, implementation of interfaces has been overhauled; I dare to say that I am close to getting it right. Time for some good copy and paste from the documentation. Here we go…

Let’s illustrate the features and limitations of interface–types by examples.

Whenever an interface inherits from another interface, it obviously becomes its sub–type:

(define-interface-type <IParent>
  (method-prototype doit
    (lambda (<number>) => (<string>))))

(define-interface-type <IChild>
  (parent <IParent>))

(type-annotation-super-and-sub? <IParent> <IChild>) ⇒ #t
(type-annotation-super-and-sub? <IChild> <IParent>) ⇒ #f

An interface can implement another interface; the implementer must declare a matching method prototype for every method prototype of the implemented. The method prototype of the implementer must be a sub–type of the method prototype of the implemented. Given that:

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

we can define:

(define-interface-type <IOne>
  (method-prototype doit
    (lambda (<nestring>) => (<number>))))

(define-interface-type <ITwo>
  (implements <IOne>)
  (method-prototype doit
    (lambda (<string>) => (<fixnum>))))

(type-annotation-super-and-sub? <IOne> <ITwo>) ⇒ #t
(type-annotation-super-and-sub? <ITwo> <IOne>) ⇒ #f

An interface can inherit method prototypes from another interface and use those to implement another interface:

(define-interface-type <I>
  (method-prototype doit
    (lambda (<string>) => (<number>))))

(define-interface-type <A>
  (method-prototype doit
    (lambda (<string>) => (<number>))))

(define-interface-type <B>
  (parent <A>)
  (implements <I>))

(type-annotation-super-and-sub? <I> <A>) ⇒ #f
(type-annotation-super-and-sub? <I> <B>) ⇒ #t
(type-annotation-super-and-sub? <A> <I>) ⇒ #f
(type-annotation-super-and-sub? <B> <I>) ⇒ #f

If the interface <ISub> inherits from <ISuper>, to implement <ISub> the interface <A> has to declare a method for every method in <ISub> and <ISuper>:

(define-interface-type <ISuper>
  (method-prototype super-doit
    (lambda (<string>) => (<number>))))

(define-interface-type <ISub>
  (parent <ISuper>)
  (method-prototype sub-doit
    (lambda (<string>) => (<fixnum>))))

(define-interface-type <A>
  (implements <ISub>)
  (method-prototype super-doit
    (lambda (<string>) => (<number>)))
  (method-prototype sub-doit
    (lambda (<string>) => (<fixnum>))))

(type-annotation-super-and-sub? <ISuper> <A>) ⇒ #t
(type-annotation-super-and-sub? <ISub>   <A>) ⇒ #t
(type-annotation-super-and-sub? <A> <ISuper>) ⇒ #f
(type-annotation-super-and-sub? <A> <ISub>)   ⇒ #f

Interface <B> implements interface <A>; interface <C> implements interface <B>; automatically, interface <C> implements interface <A>:

(define-interface-type <A>
  (method-prototype red         (lambda () => (<fixnum>))))

(define-interface-type <B>
  (implements <A>)
  (method-prototype red         (lambda () => (<fixnum>)))
  (method-prototype blue        (lambda () => (<symbol>))))

(define-interface-type <C>
  (implements <B>)
  (method-prototype red         (lambda () => (<fixnum>)))
  (method-prototype blue        (lambda () => (<symbol>))))

(type-annotation-super-and-sub? <A> <B>) ⇒ #t
(type-annotation-super-and-sub? <A> <C>) ⇒ #t
(type-annotation-super-and-sub? <B> <A>) ⇒ #f
(type-annotation-super-and-sub? <B> <C>) ⇒ #t
(type-annotation-super-and-sub? <C> <A>) ⇒ #f
(type-annotation-super-and-sub? <C> <B>) ⇒ #f

An interface that inherits from another interface can “extend” its method prototypes with additional signatures. The interface <IThree> implements the interfaces <IOne> and <ITwo>. <IThree> implements the “composite” method from <IOne> and <ITwo> with multiple method-prototype clauses:

;;
;;                 <IOne>
;;                    ^
;;                    |
;;   <IThree> +++> <ITwo>
;;
(define-interface-type <IOne>
  (method-prototype doit
    (lambda (<fixnum>) => (<string>))))

(define-interface-type <ITwo>
  (parent <IOne>)
  (method-prototype doit
    (lambda (<flonum>) => (<string>))))

(define-interface-type <IThree>
  (implements <ITwo>)
  (method-prototype doit (lambda (<fixnum>) => (<string>)))
  (method-prototype doit (lambda (<flonum>) => (<string>))))

(type-annotation-super-and-sub? <IOne>   <IThree>) ⇒ #t
(type-annotation-super-and-sub? <IOne>   <ITwo>)   ⇒ #t
(type-annotation-super-and-sub? <ITwo>   <IThree>) ⇒ #t
(type-annotation-super-and-sub? <IThree> <IOne>)   ⇒ #f
(type-annotation-super-and-sub? <ITwo>   <IOne>)   ⇒ #f
(type-annotation-super-and-sub? <IThree> <ITwo>)   ⇒ #f

the same as above, but with a single method-prototype clause:

(define-interface-type <IOne>
  (method-prototype doit
    (lambda (<fixnum>) => (<string>))))

(define-interface-type <ITwo>
  (parent <IOne>)
  (method-prototype doit
    (lambda (<flonum>) => (<string>))))

(define-interface-type <IThree>
  (implements <ITwo>)
  (method-prototype doit
    (case-lambda
      ((<fixnum>) => (<string>))
      ((<flonum>) => (<string>)))))

A record–type can implement an interface by defining a concrete method for every method prototype:

(define-interface-type <IOne>
  (method-prototype doit
    (lambda (<string>) => (<number>))))

(define-record-type <blue>
  (implements <IOne>)
  (method ({doit <number>} {S <string>})
    1))

(type-annotation-super-and-sub? <IOne> <blue>) ⇒ #t
(type-annotation-super-and-sub? <blue> <IOne>) ⇒ #f

A record–type can inherit from another record–type and use its parent’s methods to implement an interface:

(define-interface-type <I>
  (method-prototype doit
    (lambda (<string>) => (<number>))))

(define-record-type <A>
  (method ({doit <number>} {S <string>})
    1))

(define-record-type <B>
  (parent <A>)
  (implements <I>))

(type-annotation-super-and-sub? <I> <A>) ⇒ #f
(type-annotation-super-and-sub? <I> <B>) ⇒ #t
(type-annotation-super-and-sub? <A> <I>) ⇒ #f
(type-annotation-super-and-sub? <B> <I>) ⇒ #f

A record–type must implement a method for every method prototype in the interface and its parents:

(define-interface-type <ISuper>
  (method-prototype super-doit
    (lambda (<string>) => (<number>))))

(define-interface-type <ISub>
  (parent <ISuper>)
  (method-prototype sub-doit
    (lambda (<string>) => (<fixnum>))))

(define-record-type <A>
  (implements <ISub>)
  (method ({super-doit <number>} {S <string>})
    1)
  (method ({sub-doit <fixnum>} {S <string>})
    1))

(type-annotation-super-and-sub? <ISuper> <A>) ⇒ #t
(type-annotation-super-and-sub? <ISub>   <A>) ⇒ #t
(type-annotation-super-and-sub? <A> <ISuper>) ⇒ #f
(type-annotation-super-and-sub? <A> <ISub>)   ⇒ #f

The record–type <C> implements interface <B>; the interface <B> implements interface <A>; automatically, <C> implements <A>:

(define-interface-type <A>
  (method-prototype red
    (lambda () => (<fixnum>))))

(define-interface-type <B>
  (implements <A>)
  (method-prototype red
    (lambda () => (<fixnum>)))
  (method-prototype blue
    (lambda () => (<symbol>))))

(define-record-type <C>
  (implements <B>)
  (method ({red  <fixnum>})
    1)
  (method ({blue <symbol>})
    'ciao))

(type-annotation-super-and-sub? <A> <B>) ⇒ #t
(type-annotation-super-and-sub? <A> <C>) ⇒ #t
(type-annotation-super-and-sub? <B> <A>) ⇒ #f
(type-annotation-super-and-sub? <B> <C>) ⇒ #t
(type-annotation-super-and-sub? <C> <A>) ⇒ #f
(type-annotation-super-and-sub? <C> <B>) ⇒ #f

The record–type <dark-blue> inherits from <blue> the implementation of the interfaces <IOne> and <ITwo>:

;;
;;               <IOne>
;;                  ^
;;                  |
;;   <blue> +++> <ITwo>
;;     ^
;;     |
;;   <dark-blue>

(define-interface-type <IOne>
  (method-prototype ione-doit
    (lambda () => (<number>))))

(define-interface-type <ITwo>
  (parent <IOne>)
  (method-prototype itwo-doit
    (lambda () => (<symbol>))))

(define-record-type <blue>
  (implements <ITwo>)
  (fields val)
  (method ({ione-doit <number>})
    (+ 10 (.val this)))
  (method ({itwo-doit <symbol>})
    'ciao))

(define-record-type <dark-blue>
  (parent <blue>))

(define (fun-1 {O <IOne>})
  (.ione-doit O))

(define (fun-2 {O <ITwo>})
  (vector (.ione-doit O)
          (.itwo-doit O)))

(define O
  (new <dark-blue> 1))

(fun-1 O)       ⇒ 11
(fun-2 O)       ⇒ #(11 ciao)

The record–type <dark-blue> implements the interface <ITwo> and inherits from <blue> the implementation of the interface <IOne>:

;;   <blue> +++> <IOne>
;;     ^
;;     |
;;   <dark-blue> +++> <ITwo>

(define-interface-type <IOne>
  (method-prototype ione-doit
    (lambda () => (<number>))))

(define-interface-type <ITwo>
  (method-prototype itwo-doit
    (lambda () => (<symbol>))))

(define-record-type <blue>
  (implements <IOne>)
  (fields val)
  (method ({ione-doit <number>})
    (+ 10 (.val this))))

(define-record-type <dark-blue>
  (parent <blue>)
  (implements <ITwo>)
  (method ({itwo-doit <symbol>})
    'ciao))

(define (fun-1 {O <IOne>})
  (.ione-doit O))

(define (fun-2 {O <ITwo>})
  (.itwo-doit O))

(define O
  (new <dark-blue> 1))

(fun-1 O)       ⇒ 11
(fun-2 O)       ⇒ ciao

The record–type <blue> implements the interface <ITwo>, its parent <IOne>, and automatically the interface <IThree> implemented by <IOne>:

;;               <IOne> +++> <IThree>
;;                  ^
;;                  |
;;   <blue> +++> <ITwo>

(define-interface-type <IThree>
  (method-prototype ithree-doit
    (lambda () => (<string>))))

(define-interface-type <IOne>
  (implements <IThree>)
  (method-prototype ione-doit
    (lambda () => (<number>)))
  (method-prototype ithree-doit
    (lambda () => (<string>))))

(define-interface-type <ITwo>
  (parent <IOne>)
  (method-prototype itwo-doit
    (lambda () => (<symbol>))))

(define-record-type <blue>
  (implements <ITwo>)
  (fields val)
  (method ({ione-doit <number>})
    (+ 10 (.val this)))
  (method ({itwo-doit <symbol>})
    'ciao)
  (method ({ithree-doit <string>})
    "hello"))

(define (fun-1 {O <IOne>})
  (vector (.ione-doit O)
          (.ithree-doit O)))

(define (fun-2 {O <ITwo>})
  (vector (.ione-doit O)
          (.itwo-doit O)
          (.ithree-doit O)))

(define (fun-3 {O <IThree>})
  (.ithree-doit O))

(define O
  (new <blue> 1))

(fun-1 O)       ⇒ #(11 "hello")
(fun-2 O)       ⇒ #(11 ciao "hello")
(fun-3 O)       ⇒ "hello"

Two record–types in a hierarchy both implement the same interface:

(define-interface-type <Arith>
  (method-prototype add
    (lambda () => (<number>))))

(define-record-type <duo>
  (implements <Arith>)
  (fields one two)
  (method ({add <number>})
    (+ (.one this) (.two this))))

(define-record-type <trio>
  (parent <duo>)
  (implements <Arith>)
  (fields three)
  (method ({add <number>})
    (+ (.one this) (.two this) (.three this))))

(define (fun {O <Arith>})
  (.add O))

(fun (new <duo>  1 2))          ⇒ 3
(fun (new <trio> 1 2 3))        ⇒ 6

The record–type <duo> implements the interface <Stringer> which has a default method to-string:

(define-interface-type <Stringer>
  (method (to-string)
    (with-output-to-string
      (lambda ()
        (display this)))))

(define-record-type <duo>
  (implements <Stringer>)
  (fields one two)
  (custom-printer
    (lambda ({this <duo>} port sub-printer)
      (display "#[duo "    port)
      (display (.one this) port)
      (display #\space     port)
      (display (.two this) port)
      (display #\]         port))))

(define (fun {O <Stringer>})
  (.to-string O))

(fun (new <duo> 1 2))   ⇒ "#[duo 1 2]"

The record–type <duo> implements the interface <Stringer> which has a default method to-string; <duo> implements the method by itself:

(define-interface-type <Stringer>
  (method ({to-string <string>})
    (with-output-to-string
      (lambda ()
        (display this)))))

(define-record-type <duo>
  (implements <Stringer>)
  (fields one two)
  (method ({to-string <string>})
    (with-output-to-string
      (lambda ()
        (display "#[duo ")
        (display (.one this))
        (display #\space)
        (display (.two this))
        (display #\])))))

(define (fun {O <Stringer>})
  (.to-string O))

(fun (new <duo> 1 2))   ⇒ "#[duo 1 2]"

Default methods have a limitation: they cannot extend other methods. The following definitions will raise an expand–time exception:

(define-interface-type <IOne>
  (method ({doit <number>})
    1))

(define-interface-type <ITwo>
  (parent <IOne>)
  (method-prototype doit
    (lambda (<string>) => (<number>))))

because we cannot extend the default method doit in <IOne> with a method prototype in <ITwo>. The following definitions will raise an expand–time exception:

(define-interface-type <IOne>
  (method-prototype doit
    (lambda (<string>) => (<number>))))

(define-interface-type <ITwo>
  (parent <IOne>)
  (method ({doit <number>})
    1))

because we cannot extend the method prototype doit in <IOne> with a default method in <ITwo>. The following definitions will raise an expand–time exception:

(define-interface-type <IOne>
  (method ({doit <number>})
    1))

(define-interface-type <ITwo>
  (parent <IOne>)
  (method ({doit <number>} {S <string>})
    2))

because we cannot extend the default method doit in <IOne> with the default method doit in <ITwo>.

Here we use an “instantiable body” to define a “generic” interface–type:

(import (vicare language-extensions instantiable-bodies))

(define-instantiable-body define-iface-arith
  (define-interface-type <Iface>
    (method-prototype add
      (lambda () => (<type-name>)))))

(begin
  (define-iface-arith
    ((<Iface>             <NumberArith>)
     (<type-name>         <number>)))
  (define (nfun {O <NumberArith>})
    (.add O)))

(begin
  (define-iface-arith
    ((<Iface>             <StringArith>)
     (<type-name>         <string>)))
  (define (sfun {O <StringArith>})
    (.add O)))

(define-record-type <duo>
  (implements <NumberArith>)
  (fields one two)
  (method ({add <number>})
    (+ (.one this) (.two this))))

(define-record-type <string-duo>
  (implements <StringArith>)
  (fields one two)
  (method ({add <string>})
    (string-append (.one this) (.two this))))

(nfun (new <duo> 1 2))                  ⇒ 3
(sfun (new <string-duo> "hel" "lo"))    ⇒ "hello"

Next: , Previous: , Up: 2016   [Contents][Index]