Brand  X:   Generics, Interfaces, and Components
1 Introduction
1.1 Multiple Interfaces and Instance Contracts
1.2 Super Calls and Mixins
1.3 Components
1.4 Comparison with Other Libraries
2 Interfaces
define-interface
interface?
unimplemented?
interface->predicate
interface-out
3 Bundles
bundle?
bundle
bundles->properties
method-properties
define/  invoke-bundles
4 Struct Abbreviations
define-struct-abbrevs
9.3

BrandX: Generics, Interfaces, and Components🔗ℹ

 (require brandx) package: brandx-lib

This library supports interface-oriented programming in Racket via generic functions. It provides features similar to racket/generic and racket/class, and it can also replace simple uses of racket/unit, but it does not interoperate with any of those libraries. See also Comparison with Other Libraries.

1 Introduction🔗ℹ

This section introduces brandx interfaces, generic functions, and their implementations as methods associated with struct declarations. The first example uses the domain of simple geometric shapes.

First we define a shape interface with two members. Defining the interface also defines a predicate shape? and a generic function for each interface member: contains? and area.

(define-interface shape
  ([contains? (-> shape? real? real? boolean?)]
   [area (-> shape? (>=/c 0))]))

Contracts are optional, but if present they should be ordinary function contracts (use ->, ->*, etc; do not use ->m). Contracts do not affect dispatch; this library’s generic functions always dispatch on the first positional argument, so the first argument contract should generally be the interface predicate.

The interface is implemented by attaching methods to a struct declaration using #:properties and the method-properties form. The #:export clause declares the interfaces being implemented—just one, shape. Within the export, the #:all option requires that every interface member has a corresponding method definition, and the #:prefix % option indicates that the method implementations are named by prefixing the interface member name with %this avoids shadowing the generic functions. It is almost always a mistake to call an export-prefixed name; use the generic function instead.

(struct rectangle (x1 y1 x2 y2) ; x1 <= x2, y1 <= y2
  #:properties
  (method-properties
   #:export ([shape #:all #:prefix %])
 
   (define (%area self)
     (match-define (rectangle x1 y1 x2 y2) self)
     (* (- x2 x1) (- y2 y1)))
 
   (define (%contains? self x y)
     (match-define (rectangle x1 y1 x2 y2) self)
     (and (<= x1 x x2) (<= y1 y y2)))))

Note that each method has an explicit self argument. The argument name does not matter; the name self is just a convention. Unlike a class method, there is no special treatment of self, and there is no automatic access to object fields.

Calling the generic function on a rectangle instance dispatches to the rectangle method:

> (contains? (rectangle 0 0 10 20) 5 12)

#t

> (area (rectangle 1 2 11 22))

200

The interface contracts protect the generic functions from misuse:

> (contains? (rectangle 0 0 10 20) 0 'center)

contains? (generic): contract violation

  expected: real?

  given: 'center

  in: the 3rd argument of

      (-> shape? real? real? boolean?)

  contract from: (interface shape)

  blaming: top-level

   (assuming the contract is correct)

The interface contracts also protect callers from incorrect implementations. For example, the rectangle struct type does not enforce the constraint (<= x1 x2), so if we construct a bad rectangle and ask its area, we get a contract error blaming the implementation:

> (area (rectangle 5 0 0 10))

area (impl): broke its own contract

  promised: (>=/c 0)

  produced: -50

  in: the range of

      the interface member's contract

      (-> shape? (>=/c 0))

  contract from: (interface shape)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:3:0

Here is another shape implementation:

> (struct circle (xc yc r)
    #:properties
    (method-properties
     #:export ([shape #:all #:prefix %])
     (define-struct-abbrevs circle)
  
     (define (%area self)
       (* pi (sqr (.r self))))
  
     (define (%contains? self x y)
       (<= (dist-from-center self x y) (.r self)))
  
     (define (dist-from-center self x y)
       (dist (.xc self) x (.yc self) y))
  
     (define (dist x1 y1 x2 y2)
       (sqrt (+ (sqr (- x2 x1)) (sqr (- y2 y1)))))))

This implementation uses define-struct-abbrevs to make field access more convenient, by defining .xc as an alias of circle-xc, and so on. This example also shows the use of helper functions, dist-from-center and dist. The dist function could just as well have been defined outside of the method-properties body; the dist-from-center function could be moved outside also, but then it would be outside the scope of the define-struct-abbrevs aliases, so it would need to be adjusted.

Since the helper functions are not interface members, they can be named nearly anything, except that the export prefix declaration reserves all names starting with % for exports. The restriction affects all names defined immediately within the methods block. So for example, if dist were renamed to %dist, a syntax error would be raised. (This restriction helps prevent typos in intended export names from silently being ignored.)

Shape operations on circles work as expected:

> (contains? (circle 0 0 10) 3 4)

#t

> (area (circle 0 0 1))

3.141592653589793

Here is another shape implementation:

(struct union (s1 s2)
  #:properties
  (method-properties
   #:export ([shape #:prefix %])
 
   (define (%contains? self x y)
     (match-define (union s1 s2) self)
     (or (contains? s1 x y) (contains? s2 x y)))))

It is difficult to calculate the area of overlapping shapes, and it is impossible to reliably detect overlap using only the members of the shape interface anyway. So union is a partial implementation of the shape interface: it does not define a area method. Note the absence of the #:all export option. If it were present, a syntax error would raised because of the missing definition. To enforce that area is the only missing definition, an “except” clause, #:except (area), could be used instead.

A union instance is considered a shape, and it works as expected with the contains? generic:

> (shape? (union (circle 0 0 1) (rectangle 0 0 1 1)))

#t

> (contains? (union (circle 0 0 1) (rectangle 0 0 1 1)) 1/2 1/2)

#t

A call to the area generic function gets the method from union’s “super-implementation”, which is the shape interface’s fallbacks. Every interface member has a default fallback implementation which is a procedure that raises an “unimplemented” error:

> (area (union (circle 0 0 1) (rectangle 0 0 1 1)))

area: not implemented

  interface: shape

We can further “subclass” the union shape with a type that we promise to use only if we know through other means that the shapes are disjoint:

(struct disjoint-union union ()
  ; sub-shapes must be disjoint; not checked!
  #:properties
  (method-properties
   #:export ([shape #:except (contains?) #:prefix %])
   (define-struct-abbrevs disjoint-union)
 
   (define (%area self)
     (+ (area (.s1 self)) (area (.s2 self))))))

Then calls to contains? inherit the method from union and calls to area get the new implementation:

> (contains? (disjoint-union (rectangle 0 0 1 1) (rectangle 1 1 2 2)) 1/2 1/2)

#t

> (area (disjoint-union (rectangle 0 0 1 1) (rectangle 1 1 2 2)))

2

1.1 Multiple Interfaces and Instance Contracts🔗ℹ

This section illustrates additional features and patterns—multiple interface exports and instance contracts—using an example based on animals. We’ll use two behaviors of animals for this example, making noise and eating. Rather than defining a single interface, let’s define separate interfaces, one for each kind of behavior. The can-greet interface is simple:

(define-interface can-greet
  ([greet (-> can-greet? string?)]))

The interface for eating is more complicated. Different animals eat different kinds of foods, and it is wrong to feed them food that they cannot eat. One way to represent this is to have each animal (type or instance) carry a contract that describes allowable food. Then the eat operation is described by a dependent contract:

(define-interface can-eat
  ([food/c (-> can-eat? contract?)]
   [eat (->i ([self can-eat?] [food (self) (food/c self)])
             [_ void?])]))

A dog can both make noise and eat:

(struct dog ([weight #:mutable] [happiness #:mutable])
  #:transparent
  #:properties
  (method-properties
   #:export ([can-greet #:all #:prefix %]
             [can-eat #:all #:prefix %])
   (define-struct-abbrevs dog)
   (define (%greet self) (if (>= (.weight self) 10) "woof" "bark"))
   (define (%food/c self) (or/c 'dog-food 'cheese 'treat))
   (define (%eat self food)
     (case food
       [(treat) (.happiness-set! self (add1 (.happiness self)))]
       [else (.weight-set! self (add1 (.weight self)))]))))

Note that multiple exports may use the same export prefix, as above, or they may use different export prefixes. Also note that in this case, the food contract is independent of the instance, but one could also have an implementation of food/c that computes its contract from instance fields.

> (define barkly (dog 8 5))
> (greet barkly)

"bark"

> (eat barkly 'dog-food)
> (eat barkly 'cheese)
> (eat barkly 'treat)
> barkly

(dog 10 6)

> (greet barkly)

"woof"

> (eat barkly 'lettuce)

eat (generic): contract violation

  expected: (or/c (quote dog-food) (quote cheese) (quote

treat))

  given: 'lettuce

  in: the food argument of

      (->i

       ((self can-eat?)

        (food (self) (food/c self)))

       (_ void?))

  contract from: (interface can-eat)

  blaming: top-level

   (assuming the contract is correct)

1.2 Super Calls and Mixins🔗ℹ

A loud dog makes three times as much noise as a regular dog. We can define a loud-dog struct type that overrides the greet method and calls its super-implementation (the method from dog) as a helper. To get access to super-implementations, we use an #:import clause with the #:super tag.

(struct loud-dog dog ()
  #:properties
  (method-properties
   #:export ([can-greet #:all #:prefix %])
   #:import ([can-greet #:super])
   (define (%greet self)
     (define greeting (super-greet self))
     (string-append greeting " " greeting " " greeting))))

Here is a loud dog at work:

> (define princess (loud-dog 2 1))
> (greet princess)

"bark bark bark"

> (eat princess 'treat)

Notice, however, that the implementation of loudness had nothing to do with the dog or loud-dog struct type. We can extract the “loudness” behavior into a separate bundle, similar to a mixin in racket/class:

> (define loud@
    (bundle
     #:export ([can-greet #:all #:prefix %])
     #:import ([can-greet #:super])
     (define (%greet self)
       (define greeting (super-greet self))
       (string-append greeting " " greeting " " greeting))))

Then if we have another kind of animal...

(struct cat ()
  #:properties
  (method-properties
   #:export ([can-greet #:all #:prefix %]
             [can-eat #:all #:prefix %])
   (define (%greet self) "meow")
   (define (%food/c self) (or/c 'cat-food 'fish 'bird 'mouse))
   (define (%eat self food) (void))))

we can make a loud version by simply linking in the loud@ mixin bundle:

(struct loud-cat cat ()
  #:properties
  (method-properties
   #:link (list loud@)))

Here is a loud cat at work:

(greet (loud-cat))

"meow meow meow"

1.3 Components🔗ℹ

This section provides an example of using interfaces and bundles for component programming.

Interfaces intended for use with components should use the #:no-generics option, which omits the definition of the interface predicate and generic functions. The following is an interface for a worklist component:

(define-interface worklist
  ([empty any/c]
   [empty? (-> any/c boolean?)]
   [enqueue (-> any/c any/c any/c)]
   [dequeue (-> any/c (values any/c any/c))])
  #:no-generics)

It would be appealing to have the worklist signature contain a predicate or contract for the component’s worklist representation, like so:

; NOT SUPPORTED
(define-interface worklist
  ([worklist/c contract?]
   [empty worklist/c]
   [empty? (-> worklist/c boolean?)]
   [enqueue (-> worklist/c any/c worklist/c)]
   [dequeue (-> worklist/c (values any/c worklist/c))])
  #:no-generics)

But alas, this library does not allow interface contracts to depend on interface members.

The following stack component is one implementation of the worklist signature:

(define stack@
  (bundle
   #:export ([worklist #:prefix %])
   (define %empty null)
   (define %empty? null?)
   (define (%enqueue st v) (cons v st))
   (define (%dequeue st)
     (match st [(cons v st) (values v st)]))))

The traversal interface has a single member, traverse, which takes an initial value and a successors function, and collects all of the values reachabled from the initial value into a list.

(define-interface traversal
  ([traverse (-> any/c (-> any/c (listof any/c)) (listof any/c))])
  #:no-generics)

Here is an implementation of the traversal component that does no cycle detection. It uses the worklist component to manage its state.

(define traversal@
  (bundle
   #:export ([traversal #:prefix %])
   #:import ([worklist])
 
   (define (%traverse v get-next)
     (define q (enqueue empty v))
     (let loop ([q q])
       (cond [(empty? q)
              null]
             [else
              (define-values (v q2) (dequeue q))
              (define q3 (enqueue-all q2 (get-next v)))
              (cons v (loop q3))])))
 
   (define (enqueue-all q xs)
      (for/fold ([q q]) ([x (in-list xs)]) (enqueue q x)))))

As an example, let’s consider positive integers and define the “successors” using the following halfsies function:

(define (halfsies n)
  (define half (quotient n 2))
  (cond [(<= n 1) null]
        [(even? n) (list half)]
        [else (list half (add1 half))]))

The traversal function with a stack worklist implements depth-first search:

> (define/invoke-bundles #:bind ([traversal #:prefix dfs:]) stack@ traversal@)
> (dfs:traverse 100 halfsies)

'(100 50 25 13 7 4 2 1 3 2 1 1 6 3 2 1 1 12 6 3 2 1 1)

We could also implement a FIFO queue worklist component:

(define queue@
  (bundle
   #:export ([worklist #:prefix %])
   (struct queue (r w))
   (define %empty (queue null null))
   (define (%empty? q)
     (match q [(queue '() '()) #t] [_ #f]))
   (define (%enqueue q v)
     (match q
       [(queue r w) (queue r (cons v w))]))
   (define (%dequeue q)
     (match q
       [(queue '() '()) (error 'remove "empty queue")]
       [(queue (cons v r) w) (values v (queue r w))]
       [(queue '() w) (%dequeue (queue (reverse w) '()))]))))

If we link the traversal component with that instead, we get a breadth-first search:

> (define/invoke-bundles #:bind ([traversal #:prefix bfs:]) queue@ traversal@)
> (bfs:traverse 100 halfsies)

'(100 50 25 12 13 6 6 7 3 3 3 4 1 2 1 2 1 2 2 1 1 1 1)

1.4 Comparison with Other Libraries🔗ℹ

Improvements over racket/generic: This library has better binding ergonomics: implementations may use export prefixes to avoid shadowing generic functions, and multiple interfaces may be implemented in a single shared definition scope. Contracts are associated with interface members, and interface imports and exports are contract boundaries. Calls to super-methods are supported. Abstraction in the style of mixins and traits is supported at the granularity of interfaces.

Limitations compared to racket/generic: This library’s generic functions always dispatch on their first positional argument, and they do not support “defaults” (instead, define a wrapper function). Method redirection (as with redirect-generics etc) is not supported.

Differences from racket/class: Users retain direct access to structs, including pattern-matching via match. All method names to be visible externally or visible to subclasses must be declared in an interface. There is no syntactic restriction or special treatment of methods; in particular, there is no implicit this variable. Consequently, there is no syntactic support for treating fields as variables, and there is no special treatment of calls on the same object. There is no special construction/initialization support. There is no support for final methods, abstract methods, or augment methods.

2 Interfaces🔗ℹ

syntax

(define-interface iname maybe-supers maybe-predicate
  (member-decl ...) clause ...)
 
maybe-supers = 
  | #:super (super-interface-id ...)
     
maybe-predicate = 
  | #:predicate predicate-id
     
member-decl = member-id
  | [member-id maybe-contract]
     
maybe-contract = 
  | contract-expr
     
clause = #:fallbacks fallbacks-table-expr
  | #:no-generics
  | #:generics-prefix prefix-id
Defines an interface named iname with the given members. Specifically, the following names are defined:
  • iname The interface. When an interface name is used in expression position, it evaluates to the interface’s run-time representation.

  • predicate-id, if given, or else iname? A predicate that recognizes instances of struct implementing the interface.

  • member-id, prefixed with prefix-id, if given — A generic function for each member of the interface. If the #:no-generics option was given, no generics are defined.

If the #:super clause is given, then each super-interface-id must be the name of a previously-defined interface, and the interface iname extends the given interfaces. That is, an import or export of the interface implicitly imports or exports all of its super-interfaces as well.

The members of the interface are named according to the member-ids. If a member has a contract, the contract protects the generic function as well as any import or export of the interface member.

If a #:fallbacks clause is present, then fallbacks-table-expr must evaluate to a hash mapping member name symbols to their fallback implementations. Any member that is not given a fallback implementation has a default fallback value, recognized by unimplemented?, which raises an error when applied. Fallbacks cannot be given for super-interface names.

By default, a generic function is defined for every member name. If a #:no-generics clause is given, then no generics are defined. If a #:generics-prefix clause is given, then the generic function names are formed by adding the given prefix to the beginning of the member name.

procedure

(interface? v)  boolean?

  v : any/c
Returns #t is v is a run-time interface representation, #f otherwise.

procedure

(unimplemented? v)  boolean?

  v : any/c
Returns #t if v is a default fallback implementation procedure created by define-interface, #f otherwise. A default fallback procedure accepts any number of arguments and raises an “unimplemented” error.

procedure

(interface->predicate 
  ifc 
  [name 
  #:accept-struct-type? accept-struct-type?]) 
  (-> any/c boolean)
  ifc : interface?
  name : (or/c symbol? #f) = #f
  accept-struct-type? : boolean? = #f
Returns a predicate that recognizes instances of ifc. If accept-struct-type? is false (the default), then the predicate only accepts instances of structs implementing ifc; if it is true, then the predicate also accepts struct type descriptors for structs implementing ifc.

If name is a symbol, then name must be a member name of ifc, and the predicate is further constrained to only accept instances where that member name has a value that is not unimplemented?.

syntax

(interface-out interface-id)

Exports the bindings associated with the interface named by interface-id. Those bindings are interface-id itself, the interface’s predicate, and the generic functions (unless the interface was defined with #:no-generics).

3 Bundles🔗ℹ

Interfaces are implemented by bundles, which are created with the bundle form and may be invoked using define/invoke-bundles or, more typically, attached to a struct type using bundles->properties. The method-properties form provides a convenient combination of bundles->properties and bundle.

procedure

(bundle? v)  boolean?

  v : any/c
Returns #t if v is a bundle, #f otherwise.

syntax

(bundle link-clause ... definition-or-expression ...)

 
link-clause = #:export (export-spec ...)
  | #:import (import-spec ...)
  | #:link bundle-list-expr
     
export-spec = [interface-id maybe-tag maybe-complete maybe-prefix]
     
import-spec = [interface-id maybe-tag/super maybe-prefix]
     
maybe-tag = 
  | #:tag (id ...)
     
maybe-complete = 
  | #:all
  | #:except (member-id ...)
     
maybe-tag/super = maybe-tag
  | #:super
     
maybe-prefix = 
  | #:prefix prefix-id
Produces a bundle with the given exports, imports, linked bundles, and definitions.

The #:export clause declares what the bundle implements. An export consists of an interface name, an optional tag, an optional except-list, and an optional prefix. The export is satisfied by definitions in the bundle’s body matching the names of the interface members, prefixed by the export prefix, if given. An export of an interface also includes all of its super-interfaces. An exported name that has no definition in the body retains the value from the super struct type, if applicable, or the interface’s fallback implementation, otherwise.

If an export contains an #:all or #:except clause, it triggers a completeness check for the exported interface. If an #:except clause is present, then the body must contain a definition for every member except those listed. An #:all clause is equivalent to #:except (). If neither is present, then no completeness check is done.

If an export contains a non-empty prefix, then any definition in the bundle body of a name matching that prefix (and set of scopes) must correspond to an exported member name, otherwise an error is signaled. (This check helps prevent typos from causing missed exports.)

The #:import clause declares the bundle’s imports. Like an export, an import consists of an interface name, an optional tag, and an optional prefix. The special import form [interface-id #:super] is equivalent to [interface-id #:tag (super) #:prefix super-].

Imports and exports allow tags to distinguish between multiple occurrences of the same interface in the linkage graph. An import in one bundle matches an export in another bundle only if both the interface and tag matches. The default tag is (). The tag (super) is special; it is forbidden as an export tag, and as an import it is automatically satisfied by the linker using the implementation from the struct super-type (if applicable) or the interface’s fallbacks.

If a #:link clause is present, then bundle-list-expr must evaluate to a list of bundles. The linked bundles are included in the linkage graph when the enclosing bundle is linked and invoked. They may satisfy imports and consume exports of the enclosing bundle, but their imports and exports must not be duplicated in the #:import and #:export clauses of the enclosing bundle. When the bundle is invoked, the linked bundles are invoked in order before the body is evaluated.

Depending on the link order, a bundle may have imports that are not fully initialized by the time the bundle body is evaluated. In the typical case where the bundle only defines functions that refer to imports, there is no problem, but if the bundle attempts to evaluate an imported member name during initialization, it will fail if the member is exported from a bundle that has not yet been initialized.

procedure

(bundles->properties b ...)

  (listof (cons/c struct-type-property? any/c))
  b : bundle?
Links the bundles (list b ...) and returns an association list for the exported interfaces’ underlying struct type properties, suitable for the #:properties argument of the struct form. Invocation of the linked bundles is delayed until the struct is created, so that super implementations can be populated from the struct’s super type.

syntax

(method-properties link-clause ... definition-or-expression ...)

Equivalent to

(bundles->properties (bundle link-clause ... definition-or-expression ...))

syntax

(define/invoke-bundles #:bind (import-spec ...) bundle-expr ...)

Links the bundles (list bundle-expr ...), invokes them, and defines names according to the import-specs.

4 Struct Abbreviations🔗ℹ

syntax

(define-struct-abbrevs struct-id)

Defines abbreviations for the accessors and mutators of the struct type named by struct-id. Relies on struct-id being bound to compile-time information satisfying struct-info? and struct-field-info?. Aliases are also defined for accessors and mutators from super-struct types.

For each field x, an alias named .x is defined for the accessor, and an alias named .x-set! is defined for the mutator if it exists.