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

BrandX: Generics, Interfaces, and Components🔗ℹ

 (require brandx) package: brandx-lib

This library supports object-oriented and interface-oriented programming 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. (This library does not support multiple dispatch.)

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.

The implementation is implicitly packaged as a bundle; the method-properties form is a convenient composition of bundles->properties and the bundle form. Bundles are the units of implementation corresponding to interfaces and signatures.

(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 struct 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. Unlike the racket/generic library, brandx does not support attaching contracts to instances that affect the behavior of generic functions (via redirect-generics, for example). But a different kind of instance-specific contracts can be enforced using generic functions and dependent function contracts. For example, each animal can implement a get-food/c method that produces a contract describing its allowed food. Then the eat operation has a dependent contract that dynamically fetches the instance’s specific food contract using the get-food/c generic function:

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

A dog can both make noise and eat:

(struct dog (veg? [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 (%get-food/c self)
     (or/c 'dog-food 'treat (if (.veg? self) 'carrot 'cheese)))
   (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.

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

"bark"

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

(dog #f 10 6)

> (greet barkly)

"woof"

> (eat barkly 'carrot)

eat (generic): contract violation

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

cheese))

  given: 'carrot

  in: the food argument of

      (->i

       ((self can-eat?)

        (food (self) (get-food/c self)))

       (_ void?))

  contract from: (interface can-eat)

  blaming: top-level

   (assuming the contract is correct)

To summarize, brandx’s contract support focuses on operations and their implementations; this section shows how to enforce instance-specific contracts on operations. Of course, instances can also be protected directly using contracts on their representations, but the resulting contract violations will be reported in terms of the representation:

> (define/contract fifi
    (struct/c dog boolean? (between/c 0 5) real?)
    (dog #f 5 8))
> (eat fifi 'cheese)

fifi: contract violation

  expected: (between/c 0 5)

  given: 6

  in: the (#:selector dog-weight) field of

      (struct/c

       dog

       boolean?

       (between/c 0 5)

       real?)

  contract from: (definition fifi)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:29:0

1.2 Inheritance, Super Calls, and Mixins🔗ℹ

A noisy dog makes three times as much noise as a regular dog. We can define a noisy-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 noisy-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 noisy dog at work:

> (define princess (noisy-dog #t 2 1))
> (greet princess)

"bark bark bark"

> (eat princess 'treat)

Notice, however, that the implementation of noisiness had nothing to do with the dog or noisy-dog struct type. We can extract the “noisiness” behavior into a separate “mixin” bundle. (A mixin bundle is similar to a mixin in racket/class, but a mixin bundle cannot define fields.)

> (define noisy@
    (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 (%get-food/c self) (or/c 'cat-food 'fish 'bird 'mouse))
   (define (%eat self food) (void))))

we can make a noisy version by simply including the noisy@ mixin bundle:

(struct noisy-cat cat ()
  #:properties
  (method-properties
   #:compound (list noisy@)))

Here is a noisy cat at work:

(greet (noisy-cat))

"meow meow meow"

1.3 Components🔗ℹ

This section provides an example of component programming. That is, this section shows how to use brandx in a style similar to racket/unit rather than racket/generic or racket/class.

Components are described by signatures. Like an interface, a signature contains a set of member names, but unlike an interface, the signature does not define a predicate or generic functions, and it cannot be attached to a struct declaration. On the other hand, signatures directly support contracts that depend on other signature members.

The following is a signature for a worklist component:

(define-signature worklist
  ([worklist/c contract?]
   [empty #:dep (worklist/c) worklist/c]
   [empty? #:dep (worklist/c) (-> worklist/c boolean?)]
   [enqueue #:dep (worklist/c) (-> worklist/c any/c worklist/c)]
   [dequeue #:dep (worklist/c) (-> worklist/c (values any/c worklist/c))]))

A worklist component decides on a representation for worklists; the decision is made by the component, not the interface, and different worklist implementations may choose different representations. We could write approximate contracts for the operations by using any/c for worklist arguments and results, but those contracts would fail to catch many misuses of the operations. Instead, we can include a signature member, worklist/c, that allows each implementation component to declare a contract for its worklist representation, and then the signatures of the other operations depend on the worklist contract. When a signature member’s contract depends on the value of another signature member, it must declare the dependency with a #:dep clause.

The following stack@ component, which represents worklists as ordinary lists, is one implementation of the worklist signature:

(define stack@
  (bundle
   #:export ([worklist #:prefix %])
   (define %worklist/c list?)
   (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 reachable from the initial value into a list.

(define-signature traversal
  ([traverse (-> any/c (-> any/c (listof any/c)) (listof any/c))]))

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 %worklist/c queue?)
   (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)

We could also first compound queue@ and traversal@ together into a single bundle. Doing so does not hide any of their exports, and we still invoke the compound bundle in the same way:

> (define queue-traversal@ (bundle #:compound (list queue@ traversal@)))
> (define/invoke-bundles #:bind ([traversal #:prefix bfs2:]) queue-traversal@)
> (bfs2: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” (#:defaults and #:fast-defaults in define-generics); 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🔗ℹ

An interface describes a set of names that can be implemented with methods and called using generic functions. A method is a procedure that accepts at least one positional argument that is a candidate implementation for a generic function to dispatch to. Methods are attached to struct declarations using method-properties or bundles->properties.

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
  | #:generics-prefix prefix-id
Defines an interface named iname with the given members. Specifically, the following names are defined:
  • iname The interface, used mainly in import and export specifications.

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

  • member-id, prefixed with prefix-id, if given — A generic function for each member of the interface.

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 that raises an error when applied. Fallbacks cannot be given for super-interface names.

A generic function is defined for every member name. 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.

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.

3 Signatures🔗ℹ

A signature describes a set of names that can be implemented with a bundle and linked with other bundles.

syntax

(define-signature signame (member-decl ...))

 
member-decl = member-id
  | [member-id maybe-contract]
     
maybe-contract = 
  | contract-expr
  | #:dep (dep-member-id ...) contract-expr
Defines signame as a signature with the given members. Unlike define-interface, no predicate or generic functions are defined, and signatures do not support super-signatures or fallback implementations.

If a member has a contract, the contract protects imports and exports of the signature member. If the contract has a #:dep (dep-member-id ...) clause, then each dep-member-id is in scope for the evaluation of the contract expression, bound to the value of the signature member.

4 Bundles🔗ℹ

Interfaces and signatures 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 ...)
  | #:compound bundle-list-expr
     
export-spec = interface/signature-id
  | [interface/signature-id maybe-tag maybe-complete maybe-prefix]
     
import-spec = interface/signature-id
  | [interface/signature-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 or signature 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 or signature members, prefixed by the export prefix, if given. An export of an interface also includes all of its super-interfaces. A member of an exported interface 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 a member of an exported signature has no definition in the body, a syntax error is raised.

If an interface export contains an #:all or #:except clause, it triggers a completeness check for the export. 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. A signature export always performs a completeness check; the #:all option is allowed but redundant.

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 or signature name, an optional tag, and an optional prefix. The special import form [interface-id #:super] is equivalent to [interface-id #:tag (super) #:prefix super-]. If a signature import has #:super or #:tag (super), a syntax error is raised.

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

If a #:compound clause is present, then bundle-list-expr must evaluate to a list of bundles. These 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 included 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.

5 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.