They Live

We continue our exploration of Toccata types by looking at the link between deftype and defprotocol.

It's not enough to just put data together in custom types. We also need to write functions that do things to or with that data. Now, it would be possible to just write a 'plain' function that would take it's arguments and explicitly test the types of the values given to determine what to do. But there's a better way. It goes by multiple names; Parametric Polymorphism, Polymorphic Dispatch, or what have you. The idea is, at a call site, the compiler inserts code that checks the types of the argument(s) and chooses which function to execute based on the types given. This is what Toccata does. So all that remains is to tell the compiler how to distinguish between calls to 'plain' functions and calls to functions that need to be dispatched by type. That is where defprotocol comes in. It lets the programmer provide a list of functions that are to be dispatched at runtime. I wrote about Toccata's core protocols here. Toccata only chooses which function to execute based on the type of the first argument. The types of the other arguments don't factor into that decision.

So let's take one of the core protocols and look at it a little closer. Hmmmm, I pick this one.

;; For collections whose contents can be indexed by integers
(defprotocol Indexed
  (nth [coll n]
    ;; Retrieve the `n`th value from `coll`, wrapped in a Maybe, if there are
    ;; enough values in `coll`. Otherwise, returns `nothing`.
    (assert (instance? Integer n))
    (assert-result a (instance? Maybe a)))

  (store [coll n v]
    ;; Create a new copy of `coll` (wrapped in a Maybe)  with `v` at index `n`
    ;; if `coll` is at least size of `n` - 1. Otherwise, return `nothing`.
    (assert (instance? Integer n))
    (assert-result a (instance? Maybe a))))

Looking at the nth function, we see two type assertions. The first one is like we've seen before and asserts that the parameter n is always an integer. You should never put a type assertion on the first parameter to a protocol function, because it is the dispatch type.

The next thing we see is the assert-result expression. This expression includes a symbol, in this case a. This is only used inside the assertion expression and stands in for the value returned by the function.

These two assertions give us the contract for all the nth functions. They take a collection of unknown type and an integer parameter n. And they all return a Maybe value, which may be nothing or may contain the nth value of the collection. Now, if anyone tries to implement an nth function for a collection that doesn't return a Maybe value, they're going to get an error at compile time if the comipiler can determine that to be the case. Otherwise, the compiler will insert some code in that particular implementation to check the return value. Regardless, the compiler will know that any call site that calls nth will evaluate to a Maybe value. Pretty neat, huh?

Now, let's look at a type that wraps Vector for some reason.

(deftype NewVec [v]
  (assert (instance? Vector v))

  Indexed
  (nth [_ x]
    (nth v x)))

This is a nonsense type, but does show how to implement a protocol function. And since v is guaranteed to always be a Vector, the compiler can 'pre-dispatch' the call to nth in the implementation. But how is nth defined for Vector? That is here.

(extend-type Vector

  ;; ...

  Indexed
  (nth [v n]
    (get v n))

  ;; ...
  )

Vector is defined in the sub-basement of Toccata, so there's no way to write a deftype expression for it. Also, if you read through core.toc, you'll see the functionality for some of the core types has to be broken into several pieces.

More importantly, you can write an extend-type to add protocol functions to any type you want. The only difference is that in deftype expressions, you can access field values by name. Whereas in extend-type expressions, you have to use field accessors on the dispatch parameter.