derive
is a mechanism that automatically determines how to implement
interfaces for datatypes based on their members. It also allows you to write
your own rules for derive
, called a deriver
.
If you’d like to learn how to derive
interfaces for your types, read the
first section of this document. If you’d like to provide a
deriver for an interface, read the second section of
this document.
derive
In most cases, using derive
should be as simple as calling it with the type
name and interface to implement:
(deftype Point [
x Int
y Int
])
(derive Point zero)
(derive Point =)
; if you’d like to generate a different function name
; pass it as a third argument. This is useful to avoid
; name collisions
(derive Point str my-str)
The code above will provide implementations of zero
and =
for the type
Point
based on its members. The prerequisites for this to work
are that types are concrete—there are no type variables present—and its
members implement the interface. This is because the definition of both
functions hinges on the definition of its members: zero
on a type is just
zero
of all its members, equality of a type just equality of all of its
members.
Carp only provides automatic derivation of =
, zero
, and str
. Since the
code you depend on might provide other derivers, you can inspect them by
calling (derivables)
. If you want to find out if a certain interface is
derivable, you can call (derivable? <interface>)
. Please note that the
interface name needs to be quoted.
If either of the preconditions above is not met, you will have to write your
own version of these functions, and may not use derive
.
Some users might want to be able to derive update interfaces that take a type,
do the same thing to all its members, and return it. A good example for this
in the context of Point
is inc
.
While generally this might require you to write your own deriver—see section
II of this document to learn how to do that—, Carp
provides a special dynamic function called make-update-deriver
. It takes a
unary interface that updates a value and returns it, and extrapolates a
definition for the encompassing type. This is what this would look like for
Point
:
(make-update-deriver 'inc) ; notice the quote
(derive Point inc)
(inc (Point.zero)) ; => (Point 1 1)
While this can be useful at times, it is limited to the special case of
functions outlined above: it can only used on functions you would also be able
to pass into update-<member>
style functions.
Sometimes you might want to provide your own derivation strategy for other
interfaces than the ones provide out of the box. In these cases you can provide
your own deriver using make-deriver
.
The dynamic function make-deriver
takes three arguments: the quoted name of
the interface, the names of the arguments it will be passed, and a function
that, given a type, knows how to generate an implementation for that type.
This might sound a little strange, so let’s consider the deriver for zero
as
an example:
(make-deriver 'zero []
(fn [t]
(cons 'init
(map (fn [_] '(zero)) (members t)))))
It usually makes sense to read make-deriver
similar to a function definition:
its interface name is zero
, which takes no argument, and we know that if
we’re given a type we can create a definition for zero
if we just emit a
call to zero
for every member, wrapped in an init
. Thus the definition for
zero
for the type Point
from above will end up looking like this:
(init (zero) (zero))
derive
itself will emit all the surrounding boilerplate, such that the entire
call to (derive Point zero)
will be rewritten to:
(defmodule Point
(defn zero []
(init (zero) (zero)))
(implements Point.zero zero)
)
This means that all a deriver has to know is how to generate a function body when it’s given a type. Since it also has control over the argument names, it can use the arguments in its definition as well.