Putting things together

The simplest of the core protocols is Composition

  (defprotocol Composition
    (zero [x])
    (comp* [x xs]))

  (defn comp
    ([x] x)
    ([x & xs]
     (comp* x xs)))

Before digging into the semantics of this, a few things need to be explained.

  • The defprotocol expression is very similar to Clojure's.
  • Protocol functions must be fixed arity. Though there can be multiple arities specified for them.
  • Multiple arity and variadic function definitions are defined the same as they are in Clojure.

Tackling the two protocol functions first, comp* defines how to combine, or compose, two or more values to produce a new value. x is the value that determines which implementation of comp* is called. Technically, xs can be a list of values of any type. But the implementation must handle all the values in xs. This is a place where a good type system will come in handy. In the future, I intend to allow you to add assert statements to the implementations of protocol functions that will describe the conditions the arguments must satisfy. Those conditions will then be used at compile time to catch problems. But for now, just be careful. :)

In Clojure, the comp function is used only for function composition. In Toccata, it serves that purpose as well, but also allows the composition of any other data type that implements comp*. So list (or vector) concatenation, string concatenation, hash-map merging and set union, are all done using comp.


Some data types that implement comp* my have a particular value that when composed with other values of that type, do not effect the result. The empty list and empty hash-map are some examples. If that is the case, these data types should implement the zero protocol function. When called with a value of that type, it should return the 'zero' value of that type. The presence of such an implementation has some interesting implications that we'll come to in future posts.


Finally, the comp function itself is just a plain function that allows the composition API to be variadic. Nothing special about it. The 1 argument arity is just there as an optimization. comp* should still check to make sure that xs has values in it. There is an implementation issue to be aware of. When the compiler sees a call site like

  (comp [1 2] [3 4] [5 6])

It produces the AST for

  (comp* [1 2] (list [3 4] [5 6]))

This should be purely an optimization that is invisible.

And finally

Many of you are probably saying "That's just a monoid" and you're exactly right. :)