Interfaces

The :std/interface module provides the implementation of interfaces.

To use the bindings from this module:

(import :std/interface)

General

Interfaces declare the methods that an object must implement and provide a constructor to package an object together with the method implementations in a flat instance. This allows interface method invocations to bypass lookup and directly dispatch the method. Furthermore, once an interface has been instantiated for some class, the system keeps a prototype instance in cache; this allows subsequent instantiations to reduce to a single hash table lookup to the prototype which is then cloned.

Defining interfaces

In order to define an interface, you use the interface macro (see below). The macro takes a name for the interface (an identifier), together with its mixin interfaces, and a list of method signatures. Method signatures can carry contracts, which are checked at the interface boundary.

For each method, and methods coming from mixin interfaces, the macro generates a safe and an unsafe facade procedures. The safe facade checks the contracts, casts the object, and dispatches to the unchecked procedure. The unchecked procedure (which can be used directly if you want to bypass the cast and contract checks) dispatches to the actual method implementation from the instance object.

Here is an example from the Standard IO Interfaces:

(interface Closer
  (close))

(interface (Reader Closer)
  (read (u8v    :~  u8vector?)
        (start  :~ (in-range? 0 (u8vector-length u8v))               :=  0)
        (end    :~ (in-range-inclusive? start (u8vector-length u8v)) := (u8vector-length u8v))
        (need   :~  nonnegative-fixnum?                              :=  0)))

In this example we define two interfaces, Closer and Reader.

  • Closer is a base interface with a single method close that takes no arguments.
  • Reader is a refinement of Closer that defines a method read with a contract:
    • u8v is a required argument, which must satisfy the u8vector? predicate; in short, it must be a u8vector.
    • start is an optional argument, which must satisfy the (in-range? 0 (u8vector-length u8v)) predicate. Notice that optional argument contracts can be dependent on required arguments or previous optional arguments. In this case, start must be a valid index in the length range of the u8v u8vector.
    • end and need are also optional arguments with attached contracts.

Given these interface definitions, the macro will generate roughly the following:

;; Closer interface
(defsyntax Closer ...)
(def Closer::descriptor ...)
(def (Closer? obj) ...)
(def (is-Closer? obj) ...)
(def (Closer-close obj) ...)
(def (&Closer-close obj) ...)

;; Reader interface
(defsyntax Reader ...)
(def Reader::descriptor ...)
(def (Reader? obj) ...)
(def (is-Reader? obj) ...)
(def (Reader-close obj) ...)
(def (&Reader-close obj) ...)
(def (Reader-read obj arg ...) ...)
(def (&Reader-read obj arg ...) ...)
  • The Closer and Reader macros can be used to cast objects to the concrete interface implementations.
  • The Closer::descriptor and Reader::descriptor are runtime descriptors for interface reflection.
  • The Closer? and Reader? predicates can be used to check if an object is an exact instance of the respective interfaces.
  • The is-Closer? and is-Reader? can be used to check whether an object satisfies the interface (i.e. implements the required methods) and can be safely cast without causing a runtime error.
  • The Closer-close procedure casts the object to be an exact Closer instance and dispatches to &Closer-close.
  • The &Closer-close procedure is the unchecked variant, which expects an exact instance of the interface, extracts the object from the instance and dispatches to the method implementation.
  • The Reader-close procedure casts the object to be an exact Reader instance and dispatches to &Reader-close.
  • The &Reader-close procedure dispatches the method to the implementation.
  • The Reader-read procedure checks the contract, casts the object to an exact instance of Reader and dispatches to &Reader-read.
  • The &Reader-read procedure dispatches the method to the implementation.

Note

You can call the unchecked & procedures directly if you want to bypass the cast and the contract check, but you should be very careful doing that. The object must be an exact instance of your interface, and the contract must be satisfied. The underlying method implementation has no obligation to perform any checks or casts whatsoever and will likely be using unsafe operations that can crash or worse, lead to undefined behavior and launch the missiles.

When to use interfaces

First and foremost, interfaces are great for creating facades abstracting complex functionality and implementation details. They also make it easy to compose object interactions. See the standard IO interfaces for an example.

Besides the abstraction, there are also performance reasons to use interfaces. If you are interacting with an object using methods and you make many method invocations, you should consider using interfaces.

Here is an example that demonstrates the performance advantages over dynamic dispatch:

(interface Operation
  (start!)
  (apply x)
  (finish!))

(defstruct LinearAccumulator (state a b))

(defmethod {apply LinearAccumulator}
  (lambda (self x)
    (with ((LinearAccumulator state a b) self)
      (set! (LinearAccumulator-state self)
         (+ state (* a x) b)))))

(defmethod {start! LinearAccumulator}
  (lambda (self)
    (set! (LinearAccumulator-state self) 0)))

(defmethod {finish! LinearAccumulator}
  LinearAccumulator-state)


;; implementation using vanilla method dispatch
(def (fold-accumulator/vanilla acc iters)
  {start! acc}
  (for (x (in-range iters))
    {apply acc x})
  {finish! acc})

;; implementation using the Operation interface
(def (fold-accumulator/interface acc iters)
  (let (op (Operation acc))
    (Operation-start! op)
    (for (x (in-range iters))
      (Operation-apply op x))
    (Operation-finish! op)))

;; implementation using the _unchecked_ Operation interface
(def (fold-accumulator/unchecked-interface acc iters)
  (let (op (Operation acc))
    (&Operation-start! op)
    (for (x (in-range iters))
      (&Operation-apply op x))
    (&Operation-finish! op)))

And here are some timings for 1M iterations, after compiling the above code in a binary:

$ gxc -O -exe -o interface-op interface-op.ss
$ ./interface-op 1000000
(time (let () (declare (not safe)) (tmp/benchmark/interface/interface-op#fold-accumulator/vanilla _acc71_ _iters70_)))
    0.058732 secs real time
    0.058726 secs cpu time (0.058726 user, 0.000000 system)
    no collections
    64 bytes allocated
    no minor faults
    no major faults
    153354940 cpu cycles
(time (let () (declare (not safe)) (tmp/benchmark/interface/interface-op#fold-accumulator/interface _acc71_ _iters70_)))
    0.036777 secs real time
    0.036777 secs cpu time (0.036777 user, 0.000000 system)
    no collections
    448 bytes allocated
    no minor faults
    no major faults
    96028762 cpu cycles
(time (let () (declare (not safe)) (tmp/benchmark/interface/interface-op#fold-accumulator/unchecked-interface _acc71_ _iters70_)))
    0.022321 secs real time
    0.022313 secs cpu time (0.022313 user, 0.000000 system)
    no collections
    160 bytes allocated
    no minor faults
    no major faults
    58279314 cpu cycles

$ gxc -O -exe -full-program-optimization -o interface-op-optimized interface-op.ss
$ ./interface-op-optimized 1000000
(time (let () (declare (not safe)) (tmp/benchmark/interface/interface-op#fold-accumulator/vanilla _acc71_ _iters70_)))
    0.044859 secs real time
    0.044846 secs cpu time (0.042098 user, 0.002748 system)
    no collections
    64 bytes allocated
    no minor faults
    no major faults
    117129276 cpu cycles
(time (let () (declare (not safe)) (tmp/benchmark/interface/interface-op#fold-accumulator/interface _acc71_ _iters70_)))
    0.021684 secs real time
    0.021672 secs cpu time (0.021599 user, 0.000073 system)
    no collections
    704 bytes allocated
    no minor faults
    no major faults
    56617088 cpu cycles
(time (let () (declare (not safe)) (tmp/benchmark/interface/interface-op#fold-accumulator/unchecked-interface _acc71_ _iters70_)))
    0.020295 secs real time
    0.020296 secs cpu time (0.016758 user, 0.003538 system)
    no collections
    160 bytes allocated
    no minor faults
    no major faults
    52992136 cpu cycles

As we can see the interface version is significantly faster, and the difference is becomes even more pronounced with deep/wide class hierarchies.

Tips for Effective Use of Interfaces

Cast your objects and reuse them across many interface calls

Let's say an object obj implements an interface A and you have a method f you want to invoke on the interface:

(interface A
  (f ...))

(def obj ...)

;; what not to do as you will create unnecessary temporary instances from the cast.
(A-f obj ...) ; this will implicitly cast

;; what to do for better performance:
(def a (A obj))
(A-f a ...)

Use the subtype methods for interface mixins

When you have interfaces A and B mixing A, then the interface macro will define A's methods for B as well. In this case, if you have an instance of B you should use B's methods directly to avoid unnecessary casts.

(interface A
  (f ...))

(interface (B A)
 ...)

(def b (B ...))

;; what not to do, as it would incur a performance penalty and allocate a temporary instance:
(A-f b ...)

;; what to do for better performance:
(B-f b ...)

Use unchecked methods when you know the exact type and satisfy the contract

If you know the exact type of an instance and you satisfy the contract, you can elide the cast and contract checks by calling the unchecked method the directly.

Note

Note: Use with caution; if you get this wrong and the type is not the right instance or the contract is not satisfied, there will be dragons.

(interface A
  (f ...))

(def a (A ...))

;; what to do for better performance:
(&A-f a ...)

Macros

interface

(interface id method-signature ...)
(interface (id mixin-id ...) method-signature ...)

method-signature:
 (id required-argument ... optional-argument ... keyword-argument ... [. rest])

required-argument:
 id                    ; any type
 (id : Type)           ; specific Type: struct, class, or interface type
 (id :~ predicate)     ; predicate contract check

 optional-argument:
  (id default)
  (id : Type := default)
  (id :~ predicate := default)
  (id := default : Type)
  (id := default :~ predicate)

keyword-argument:
 keyword required-argument
 keyword optional-argument

This is the interface definition macro; it creates an interface with name id, optionally mixing in the methods from existing interfaces mixin-id .... The body of the interface consists of zero or more method specifications, which is the name of the method and the signature of the method, omitting self.

The macro generates a syntax binding for the interface meta information (which is also a constructor macro), a constructor, two predicates (for direct instance, <interface>? and interface satisfiability check is-<interface>?), and checked and unchcked procedure definitions for all the interface methods with the name <interface>-<method> and &<interface>-<method>. Here <interface> means the symbolic name of the interface.

Here is an example:

(interface Operation
  (start!)
  (apply x)
  (finish!))

; => expands to the following definitions:

(defsyntax Operation ...)
(def Operation::t ...)
(def Operation::descriptor ...)
(def (make-Operation obj) ...)
(def (Operation? obj) ...)
(def (is-Operation? obj) ...)
(def (Operation-start! self) ...)
(def (&Operation-start! self) ...)  ; unchecked
(def (Operation-apply self x) ...)
(def (&Operation-apply self x) ...) ; unchecked
(def (Operation-finish! self) ...)
(def (&Operation-finish! self) ...) ; unchecked

interface-out

(interface-out interface-id ...)
(interface-out unchecked: #t interface-id ...)

Export macro for exporting the symbols related to an interface. By default, unchecked method stubs are not exported; you will need to use the second form to also export them.

interface-instance?

(interface-instance? obj)

Predicate that checks whether an object is an instance of an interface.

interface-instance-object

(interface-instance-object instance)

Extracts the underlying object from an interface instance.

interface-descriptor?

(interface-descriptor? obj)

Predicate that checks whether an object is an interface descriptor.

interface-descriptor-type

(interface-descriptor-type descriptor)

Returns the class type of exact instances of the interface described by the descriptor.

interface-descriptor-methods

(interface-descriptor-methods descriptor)

Returns the list of methods in the interface; a list of symbols.

interface-cast-error?

(interface-cast-error? obj)

Predicate that checks whether an exception condition is a interface cast failure.