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 methodclose
that takes no arguments.Reader
is a refinement ofCloser
that defines a methodread
with a contract:u8v
is a required argument, which must satisfy theu8vector?
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 theu8v
u8vector.end
andneed
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
andReader
macros can be used to cast objects to the concrete interface implementations. - The
Closer::descriptor
andReader::descriptor
are runtime descriptors for interface reflection. - The
Closer?
andReader?
predicates can be used to check if an object is an exact instance of the respective interfaces. - The
is-Closer?
andis-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 exactCloser
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 exactReader
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 ofReader
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.
Procedures related to interfaces
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.