Previous: , Up: interfaces   [Contents][Index]


10.4 Interfaces by examples

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"

Previous: , Up: interfaces   [Contents][Index]