It’s been a while since I’ve written a technical blog post, and even longer since I’ve blogged about Carp. Today’s blog post will right both of these wrongs; I’ll talk about how one could implement derive
in Carp.
Firstly, a little context is in order. Haskell has a concept called type classes, which is a way of defining generic functions that different types can implement. An example for that would be Eq
, the equality typeclass. All types that need to be able to be compared for equality using ==
have to implement this type class and supply a definition of that function. This is similar to how interface functions in Carp work—interfaces are functions that can have multiple specialized implementations. In fact, the definitions of equality look relatively alike in Haskell and Carp.
Look at the Haskell version:
-- the actual Eq typeclass also defines /=
class Eq a where
(==) :: a -> a -> Bool
And then the Carp version:
(definterface = (λ [a a] Bool))
Aside from the fact that Carp uses =
for equality checking, this looks fairly similar1.
Something that I sorely missed in Carp for a long time was deriving
. What this does is automatically infer definitions of typeclasses based on a type. This only works for a certain set of typeclasses, but it’s fairly useful, since those typeclasses are among the most important: Show
defines a functions for converting a value into a String
, and Eq
and Ord
define functions for comparing values, among others.
Carp generates some functions automatically when a type is defined, like str
(though you can override those definitions manually if you so desire). To me, this has always been almost the best of both worlds: these utility functions are always available if I need them, and if I need to I can still write better versions by hand.
What I wanted to tackle though was deriving of similarly useful, but less central functions, like zero
—a function for generating the zero
value of a type, such as 0
for integers and ""
for strings.
So obviously because I’m me and I love macros, I wrote a few macros to emulate something akin to deriving arbitrary functions! Let’s walk through them!
An API
The API of an ideal derive
mechanism is quite simple. We’d like to have one form, derive
, that just takes the type and the function to infer.
(deftype T [
x Int
y Int
])
(derive T inc)
(derive T dec)
In the example in Figure 3, both members of the type would be incremented by the function. Of course this wouldn’t always be sensible, but probably a relatively useful default.
A prerequisite for this to work is that all types inside the type T
need to implement inc
, either by inference or definition. We won’t derive recursively.
To make our first implementation simple, let’s start by assuming that all functions that can be derived have the signature (λ [a] a)
, meaning that they take a thing of that type and return another. This isn’t strictly necessary and rules out a bunch of useful functions such as zero
described above, but it simplifies our implementation. I’ll also give a definition of a mechanism for deriving more complex functions such as =
and zero
at the end.
An Implementation
As always, we’ll start with a skeleton. The first thing to do is to define a macro derive
that takes a type t
and a function f
. We’ll also already open the module of the type, so that the implementation of f
is correctly encapsulated, and define an empty function.
(defmacro derive [t f]
(list 'defmodule t
; o is an arbitrary name. because we open
; a new function, we don’t really have to
; worry about hygiene
(list 'defn f (array 'o)
; and now?
)
)
)
derive
.
Inside that function, we’ll probably have to go through the members recursively. Recursion in Carp macros is a little tricky, and dynamic functions—that is, functions defined using defndynamic
that are available at compile time—are probably a better fit. So let’s defer to such a function inside the macro.
(defmacro derive [t f]
(list 'defmodule t
(list 'defn f (array 'o)
(derive-internal f t))))
derive
.
And that’s it for derive
!
Of course we’re just getting started. The actual meat of the mechanism happens inside derive-internal
. Well, as it turns out, that’s also not true. derive-internal
will be a trampoline for the real recursive function, and its sole job will be to gather the members of the type. So, how do we get at them? Luckily, Carp has a compile-time function called members
that returns an array of pairs from member name to type. For T
in Figure 3, for instance, members
would return [(x Int) (y Int)]
. It looks like this is just what we need!
(defndynamic derive-internal [f a]
(derive-internal2 f (members a)))
Fig. 6: derive-internal
, the trampoline function.
Alright, this is starting to look interesting! We have the functions we want to derive, and the members of the type we need to work on. That’s good! Now it’s time to think about the actual body of such a generated function. We could define it imperatively, updating one member after the other, resetting a result value. This might come naturally to those coming to Carp from C or even Rust.
(defn inc-imperative [o]
(let [res o]
(do
(set! res (T.set-x res (inc (T.x &res))))
(set! res (T.set-y res (inc (T.y &res))))
res)))
inc
for T
.
This is fairly clunky, and wouldn’t work with the ownership model of Carp. The version I come up with is a style that looks very straightforward, but gets a little tedious to write out manually for big types. For T
, it would look like this:
(defn inc-functional [o]
(T.update-y
(T.update-x o &inc)
&inc))
inc
for T
.
This version leverages the update-*
functions that Carp generates for all type members. Those functions always take an object of that type and a reference to a function that updates the member. This means that (T.update-x (T.init 1 2) &inc)
, for instance, would return (T 2 2)
.
With a plan in mind let’s define the awkwardly named function derive-internal2
!
(defndynamic derive-internal2 [f ms]
(if (= (length ms) 0)
'(init)
; now what?
)
)
Fig. 9: A skeleton version of derive-internal2
.
First, let’s make sure that we deal with all cases. If a type has no members, we just return a new, empty type. This doesn’t make a lot of sense for inc
, but it’s as best a guess as any, I suppose.
Now let’s deal with the base case: one last member is left.
(defndynamic derive-internal2 [f ms]
(if (= (length ms) 0)
'(init)
(if (= (length ms) 1)
(list (Symbol.join ['update- (caar ms)])
'o
(list 'ref f))
; now what?
)
)
)
derive-internal2
.
Once we have just one member left, we take the name of the member using caar
—because it’s the first element of a pair inside an array—, prefix it with update-
, stick in o
, which we decided would be the name of our parameter in Figure 5, and also take a reference to the function f
that we’re trying to infer. We’ll end up with a call that looks a little like (T.update-x o (ref inc))
2.
That wasn’t too bad. In the recursive case, we’ll do something similar, but we’ll also, well, recurse.
(defndynamic derive-internal2 [f ms]
(if (= (length ms) 0)
'(init)
(if (= (length ms) 1)
(list (Symbol.join ['update- (caar ms)])
'o
(list 'ref f))
(list (Symbol.join ['update- (caar ms)])
(derive-internal2 f (cdr ms))
(list 'ref f)))))
derive-internal2
in its final form.
All that changes in the recursive case is that we don’t pass in o
anymore. Instead, we will give the result of the inner computations to our updater.
And that’s all there is to it!
Caveats
This version of derive
is still fairly limited. It only works for a subset of all possible interface functions, some of the time. If you want to see how this could maybe be generalized even more, I’ve created special cases for derive
for zero
and =
that use a bit of complex machinery. You can study them on your own, at your own leisure, if you want to. I couldn’t quite figure out how to completely generalize them yet, sadly, but I suppose that could be left as an exercise to the reader!
Fin
As always, thank you for reading my blog! I had a lot of fun playing around with Carp’s macro machinery and bending it until it breaks. I’ve discovered a few bugs in the process, some of which are fixed, some of which have not yet been fixed.
Working with macros in Carp definitely feels different than working with Scheme macros; in some ways, it was a similar experience to writing elaborate macros in Clojure, but with types added to the mix. It’s quite the experience, and I wholeheartedly recommend trying it out!
Footnotes
1. Carp interfaces do not support grouping of functions; every interface stands on its own. Haskell’s typeclasses, on the other hand, often have more than one function encapsulated within them. This is useful because some functions don’t make sense without other ones—think of monadic bind without return—, but Carp omits this for now.
2. (ref f)
is equivalent to &f
.