Super-types and sub-types

Posted on Sat Jun 25, 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.

Super–types and sub–types

Done a crackdown on the machinery that determines when two expand–time type specifications, or two run–time type descriptors, are super–type and sub–type. There are plenty of built–in syntaxes to check this relation.

Given the core types <number>, <fixnum> and <string>, we have:

(type-annotation-matching <number> <fixnum>)    ⇒ exact-match
(type-annotation-matching <fixnum> <number>)    ⇒ possible-match
(type-annotation-matching <number> <string>)    ⇒ no-match

it means that the following code will type–check at expand–time, and no type checking is performed at run–time on the operand ‘123’:

(define (fun {O <number>})
  O)

(fun 123)       ⇒ 123

the following code will type–check at expand–time, and the operand in the application of fun is checked at run–time:

(define (fun {O <fixnum>})
  O)

(define ({rand <number>})
  123)

(fun (rand))

the following code will raise an expand–time violation:

(define (fun {O <fixnum>})
  O)

(fun "ciao")    error→ expand-time type mismatch

There are many type annotation variants, so there are many rules to compare type specifications and type descriptors. Special care is needed when defining type annotations for closure objects that must be used as operands in function calls. The general rule is this:

(type-annotation-super-and-sub? <number> <fixnum>)      ⇒ #t
(type-annotation-super-and-sub? <struct> <record>)      ⇒ #t

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

we see that:

in other words: the super–type’s argument must be equal or stricter; the sub–type’s return value must be equal or stricter.

Scheme is, originally, a dynamically typed language; so, sometimes, we may need a type specification describing a closure object that accepts arguments of any type. Given the comparison rules for super–types and sub–types, this appears problematic because the super–type’s arguments must have types that are sub–types of any other type.

For this we can use the special type annotation <bottom>, which is a conventional sub–type of all the other types; for example the type annotation:

(define-type <my-func>
  (lambda (<bottom>) => (<string>)))

is a super–type of both number->string and symbol->string, because <number> and <symbol> are super–types of <bottom>.

In addition, the improper list <bottom> is now used to represent the type of return values for expressions that do not return (for example: they raise an exception). So now:

(type-of error)
⇒ #[signature ((lambda ((or <false> <symbol> <string>) <string> . <list>)
                       => <bottom>))]

(type-of (error #f "wrong"))
⇒ #[signature <bottom>]

in previous revisions Vicare used <no-return> for this purpose, but now <no-return> has been removed.

Farewell slot-set and slot-ref!

I have removed slot-set and slot-ref!; with the typed language’s dot notation to access record’s and struct’s fields, there is no need to keep such syntaxes. Also, they say that having a uniform syntax to access an object’s interface makes code more maintainable (Uniform access principle).

New constructor-signature clause for define-record-type

I have implemented the constructor-signature clause to specify the type signature of record–type constructor procedures:

(define-record-type <alpha>
  (fields {A <fixnum>}
          {B <string>})
  (protocol
    (lambda (make-record)
      (lambda (A)
        (make-record A (number->string A)))))
  (constructor-signature
    (lambda (<fixnum>) => (<alpha>))))