(lispkit type)

Library (lispkit type) provides a simple, lightweight type abstraction mechanism. It allows for creating new types at runtime that are disjoint from all existing types. The library provides two different types of APIs: a purely procedural API for type creation and management, as well as a declarative API which allows for introducing extensible types in a declarative fashion.

A core feature of the type abstraction mechanism is a means that allows for determining the type of any LispKit value. Procedure type-of implements this type introspection feature. It returns a type tag, i.e. a symbol representing the type, for any given object.

Usage of the procedural API

New types are created with procedure make-type. make-type accepts one argument, which is a type label. The type label is either a string or a symbol that is used for debugging purposes but also for defining the string representation of a type tag that represents the new type.

The following line introduces a new type for intervals:

(define-values (interval-type-tag
                new-interval
                interval?
                interval-ref
                make-interval-subtype)
  (make-type 'interval))

(make-type 'interval) returns five values:

  • interval-type-tag is an uninterned symbol with the string representation "interval" which represents the new type.

  • new-interval is a procedure which takes one argument, the internal representation of the interval, and returns a new object of the new interval type.

  • interval? is a type test predicate which accepts one argument and returns #t if the argument is of the new interval type, and #f otherwise.

  • interval-ref takes one object of the new interval type and returns its internal representation. interval-ref is the inverse operation of new-interval.

  • make-interval-subtype is a type generator (similar to make-type), a function that takes a type label and returns five values representing a new subtype of the interval type.

Now it is possible to implement a constructor make-interval for intervals:

(define (make-interval lo hi)
  (if (and (real? lo) (real? hi) (<= lo hi))
      (new-interval (cons (inexact lo) (inexact hi)))
      (error "make-interval: illegal arguments" lo hi)))

make-interval first checks that the constructor arguments are valid and then calls new-interval to create a new interval object. Interval objects are represented via pairs whose car is the lower bound, and cdr is the upper bound. Nevertheless, pairs and interval objects are distinct values as the following code shows:

(define interval-obj (make-interval 1.0 9.5))
(define pair-obj (cons 1.0 9.5))

(interval? interval-obj)        ⇒ #t
(interval? pair-obj)            ⇒ #f
(equal? interval-obj pair-obj)  ⇒ #f

The type is displayed along with the representation in the textual representation of interval objects: #interval:(1.0 . 9.5).

Below are a few functions for interval objects. They all use interval-ref to extract the internal representation from an interval object and then operate on the internal representation.

(define (interval-length interval)
  (let ((bounds (interval-ref interval)))
    (- (cdr bounds) (car bounds))))

(define (interval-empty? interval)
  (zero? (interval-length interval)))

The following function calls show that interval-ref fails with a type error if its argument is not an interval object.

(interval-length interval-obj)
⇒ 8.5
(interval-empty? '(1.0 . 1.0))
⇒ [error] not an instance of type interval: (1.0 . 1.0)

Usage of the declarative API

The procedural API provides the most flexible way to define a new type in LispKit. On the other hand, this approach comes with two problems:

  1. a lot of boilerplate needs to be written, and

  2. programmers need to be experienced to correctly encapsulate new data types and to provide means to extend them.

These problems are addressed by the declarative API of (lispkit type). At the core, this API defines a syntax define-type for declaring new types of data. define-type supports defining simple, encapsulated types as well as provides a means to make types extensible.

The syntax for defining a simple, non-extensible type has the following form:

(define-type name name? ((make-name x ...) expr ...) name-ref functions)

name is a symbol. It defines the name of the new type and the identifier name is bound to a type tag representing the new type. name? is a predicate for testing whether a given object is of type name. make-name defines a constructor which returns a value representing the data of the new type. name-ref is a function to unwrap values of type name. It is optional and normally not needed since functions can be declared such that the unwrapping happens implicitly. All functions defined via define-type take an object (usually called self) of the defined type as their first argument.

There are two forms to declare a function as part of define-type: one providing access to self directly, and one only providing access to the unwrapped data value:

((name-func self y ...) expr ...)

provides access directly to self (which is a value of type name), and

((name-func (repr) y ...) expr ...)

which provides access only to the unwrapped data repr.

With this new syntax, type interval from the section describing the procedural API, can now be re-written like this:

(define-type interval
  interval?
  ((make-interval lo hi)
    (if (and (real? lo) (real? hi) (<= lo hi))
        (cons (inexact lo) (inexact hi))
        (error "make-interval: illegal arguments" lo hi)))
  ((interval-length (bounds))
    (- (cdr bounds) (car bounds)))
  ((interval-empty? self)
    (zero? (interval-length self))))

interval is a standalone type which cannot be extended. define-type provides a simple means to make types extensible such that subtypes can be created reusing the base type definition. This is done with a small variation of the define-type syntax:

(define-type (name super) name? ((make-name x ...) expr ...) name-ref functions)

In this syntax, super refers to the type extended by name. All extensible types extend another extensible type and there is one supertype called obj provided by library (lispkit type) as a primitive.

With this syntactic facility, interval can be easily re-defined to be extensible:

(define-type (interval obj)
  interval?
  ((make-interval lo hi)
    (if (and (real? lo) (real? hi) (<= lo hi))
        (cons (inexact lo) (inexact hi))
        (error "make-interval: illegal arguments" lo hi)))
  ((interval-length (bounds))
    (- (cdr bounds) (car bounds)))
  ((interval-empty? self)
    (zero? (interval-length self))))

It is now possible to define a tagged-interval data structure which inherits all functions from interval and encapsulates a tag with the interval:

(define-type (tagged-interval interval)
  tagged-interval?
  ((make-tagged-interval lo hi tag)
    (values lo hi tag))
  ((interval-tag (bounds tag))
    tag))

tagged-interval is a subtype of interval; i.e. values of type tagged-interval are also considered to be of type interval. Thus, tagged-interval inherits all function definitions from interval and defines a new function interval-tag just for tagged-interval values. Here is some code explaining the usage of tagged-interval:

(define ti (make-tagged-interval 4.0 9.0 'inclusive))
(tagged-interval? ti)       ⇒ #t
(interval? ti)              ⇒ #t
(interval-length ti)        ⇒ 5.0
(interval-tag ti)           ⇒ inclusive
(interval-tag interval-obj)
⇒ [error] not an instance of type tagged-interval: #interval:((1.0 . 9.5))

Constructors of extended types, such as make-tagged-interval return multiple values: all the parameters for a super-constructor call and one additional value (the last value) representing the data provided by the extended type. In the example above, make-tagged-interval returns three values: lo, hi, and tag. After the constructor make-tagged-interval is called, the super-constructor is invoked with arguments lo and hi. The result of make-tagged-interval is a tagged-interval object consisting of two state values contained in a list: one for the supertype interval (consisting of the bounds (lo . hi)) and one for the subtype tagged-interval (consisting of the tag). This can also be seen when displaying a tagged-interval value:

ti ⇒ #<tagged-interval (4.0 . 9.0) inclusive>

This is also the reason why function interval-tag gets access to two unwrapped values, bounds and tag: one (bounds) corresponds to the value associated with type interval, and the other one (tag) corresponds to the value associated with type tagged-interval.

Type introspection

LispKit defines a type tag for every different type of object. The type of objects created with the make-type facility are represented with unintered symbols whose string representation matches the type label provided to make-type. Enum and record objects come with a corresponding type tag as well. All standard, built-in types use interned symbols as type tags. Procedure type-of returns the type tag associated with the value of the argument of type-of.

Returns a list of type tags, i.e. symbols, associated with the type of obj. The type tags in the list are sorted, starting with the most specific type. If no type can be determined, type-of returns the empty list.

Type tags of custom types are returned as the first value of make-type. Type tags of records can be retrieved via record-type-tag from the corresponding record type object. Similarly, type tags of enum objects are accessible via enum-type-type-tag. Type tags of native types are typically exported by the libraries providing access to the type. All standard, built-in types are represented by the following interned symbols: void, end-of-file, null, boolean, symbol, fixnum, bignum, integer, rational, flonum, real, complex, number, char, string, bytevector, pair, list, box, mpair, array, vector, gvector, values, procedure, parameter, promise, environment, hashtable, port, input-port, output-port, record-type, and error. For undefined values #f is returned.

Type management

Based on a string or symbol type-label, creates a new, unique type, and returns a type tag representing the new type as well as five values dealing with this new type:

  1. The first value is a type tag, i.e. an uninterned symbol representing the new type which has the same string representation as type-label.

  2. The second value is a unary procedure returning a new object of the new type which is wrapping the argument of the procedure (i.e. the internal representation of the new type).

  3. The third value is a type test predicate which accepts one argument and returns #t if the argument is of the new type, and #f otherwise.

  4. The fourth value is a procedure which takes one object of the new type and returns its internal representation (that was passed to the procedure returned as the second value).

  5. The fifth value is a type generator procedure (similar to make-type), a function that takes a type label and returns five values representing a new subtype of the new type.

type-label has two uses: it is used for debugging purposes and determines the string representation of the corresponding type tag. It is shown when an object's textual representation is used. In particular, calling the third procedure (the type de-referencing function) will result in an error message exposing the type label if the argument is of a different type than expected.

Defines a new standalone type name consisting of a type test predicate name?, a constructor make-name, and an optional function ref used to unwrap values of type name. ref is optional and normally not needed since functions func can be declared such that the unwrapping happens implicitly. All functions func defined via define-type take an object (usually called self) of the defined type as their first argument.

There are two ways to declare a function as part of define-type: one providing access to self directly, and one only providing access to the unwrapped data value:

  • ((name-func self y ...) expr ...) provides access directly to self (which is a value of type name), and

  • ((name-func (repr) y ...) expr ...) provides access only to the unwrapped data repr.

This variant of define-type defines a new extensible type name extending supertype super, which also needs to be an extensible type. A new extensible type name comes with a type test predicate name?, a constructor make-name, and an optional function ref used to unwrap values of type name. ref is optional and normally not needed since functions func can be declared such that the unwrapping happens implicitly. All functions func defined via define-type take an object (usually called self) of the defined type as their first argument.

There are two ways to declare a function as part of define-type: one providing access to self directly, and one providing access to the unwrapped data values (one for each type in the supertype chain):

  • ((name-func self y ...) expr ...) provides access directly to self (which is a value of type name), and

  • ((name-func (repr ...) y ...) expr ...) provides access only to the unwrapped data values repr.

Constructors of extended types return multiple values: all the parameters for a super-constructor call and one additional value (the last value) representing the data provided by the extended type.

The supertype of all extensible types defined via define-type. The type tag of obj can be retrieved via (type-of obj).

Returns the type tag associated with the supertype of all extensible types obj.

Returns #t if obj is an instance of an extensible type. For example, (extensible-type? obj) returns #t.

Returns the type tag associated with the extensible type etype defined by the define-type form.

Last updated