Classy Units
Haskell type classes are a very nice take on ad hoc polymorphism. Much like overloading in object oriented languages, type classes provide a mechanism for different implementations of the same conceptual operation to go by the same name.
A main difference is the total separation of interface from state representation. With class-based object orientation, programmers are often encouraged to define some thing, a class, that is a bit of state along with operations for manipulating that state in various ways. At some point, the programmer may want variations of those manipulations, and so can delve into inheritance. If more flexibility is needed (namely, the state representation shouldn’t be inherited), the programmer can turn to interfaces in languages like C#, Java, etc.
Now the programmer can program to the interface, and have wildly different state representations implement that interface. Type classes support that style of development in Haskell, but one can gain some of the same operational benefits by working with Units in PLT Scheme. Since I’ve talked about Units before, let’s just get right into this example of their usage.
Suppose I want to overload arithmetic operators in a very ad hoc way. More specifically, when I define a new data structure I want the freedom to define addition over that data structure such that I can write another function that uses the + operator without worrying about having to call a different + for different data structures. There are many ways of solving this kind of problem, this is but one.
#lang scheme (require (prefix-in base: (only-in scheme + /))) (require "typeclass-units.ss") ;; A class of things for which addition is defined. (define-signature addable^ (zero +)) ;; A class of things for which division is defined. (define-signature divisible^ (/)) ;; Standard arithmetic on numbers. (define-instance (addable^ divisible^) num@ (define zero 0) (define + base:+) (define / base:/)) ;; Addition over strings is defined as string-append. (define-instance addable^ string@ (define zero "") (define + string-append)) ;; The "+" function is a generic addition operation. (define-constrained (+ (addable^) => x y) (+ x y)) ((+ num@) 3 4) ; Adding numbers ((+ string@) "hi " "there") ; Adding strings ;; A helper combinator. (define (flip f) (λ(x y) (f y x))) ;; The definition of the "mean" function requires both addition and division. (define-constrained (mean (addable^ divisible^) => items) (/ (foldl (flip +) zero items) (length items))) ;; Computing the mean of a list of numbers. ((mean num@ num@) '(4 5 6)) ;; Try giving an incompatible instance of divisible^... doesn't work. ((mean string@ num@) '("what " "is " "an average " "string?"))
What’s going on here? Let’s start from near the bottom with the mean function that computes the arithmetic mean (average) of a list of items. In order to write this function the way I have chosen to write it, I require definitions of addition and division for the items in the list. My function is not just,
(define (mean items) ...)
as that implies that the function is defined for any type of items. Instead, I want to let the caller of this function supply me with suitable definitions of addition and division for the items in question. To this end, I define mean to be constrained to the cases where the caller can supply such definitions.
This is all tied together through the Units system. In fact, this usage of Units is quite a bit restricted, but I think the syntax for this simpler usage is commensurately simpler, so perhaps of some use.
To define our interface, we use stock Unit signatures such as,
(define-signature addable^ (zero +))
that says that an addable thing has definitions for an additive identity, zero, and a definition of the + operator. To implement this interface, or define an instance of the type class, we can use a form like,
(define-instance addable^ string@ (define zero "") (define + string-append))
that defines a + operator and a zero for strings which I can use in expressions like,
((+ string@) "hi " "there")
to yield the string "hi there".
Definitions of + and / for numbers are straightforward, but I’m not sure what I would want division over strings to mean, so I haven’t defined it. If I wanted to try computing a mean of a string, I would need to supply an instance definition of divisible^, so I tried giving it one for numbers at the end of that code listing… which didn’t work because the / from num@ doesn’t work for strings.
Perhaps, though, agreement on a name alone is not enough. We may want to impose some limitations on how somebody can implement an interface, and thankfully we can do just this by attaching contracts to our signatures.
Let’s say that we’re in a punchy mood and define a “shape” to be anything that has an area,
(define-signature shape^ (area))
We then find ourselves dealing with some joker who gave us an instance like this,
(define-instance shape^ bad@ (define (area b) "I'm not an area!"))
Clearly, this is not an area.
To stop this sort of thing before it catches on, we can tighten our interface with a contract,
;; Shapes have area (define-signature shape^ ((contracted [area (-> any/c number?)])))
Now, when we call the bad@ implementation of area, we get this error before the return value gets back into our code,
(unit bad@) broke the contract (-> any/c number?) on area; expected <number?>, given: "I'm not an area!"
Significant caveats:
- Instance passing is explicit, putting a burden on the caller of a constrained function.
- The simplified Unit syntax hides the lexical binding of the instance definitions supplied to constrained functions.
- There is no mechanism to implicitly pass supplied instance definitions to constrained functions called by other constrained functions.
This means that the functions that have constraints are, with this implementation, treated as terminal. Dynamic dispatch, as with multi-methods, can offer nicer call-site syntax, but the approach to dynamically binding implementations to identifiers shown here allows for a single resolution of that dispatch to be captured in a closure. If I have a large block of code dependent on some set of signatures, I can provide the relevant implementations and get a specialized closure back. For instance, the expression,
(+ string@)
results in a function for which references to the + function are bound to the implementation defined for strings. This is just the kind of thing that Units excel at, and I hope that the uses shown here can, if nothing else, encourage others to explore specific applications of the power of Units.
While developing the macro support for this Unit usage pattern, I played with several experiments which demonstrate different approaches to working with interfaces defined in this manner, some very much like OO classes.
The simplest example is one that implements a funny head function for both lists and vectors. The arithmetic example is next up, followed by the much more object oriented shapes example.
The little macros that provide this syntax are defined in the typeclass-units module.