A critique of types as entities
Rich Hickey has a really nice talk called Clojure Made Simple: Clojure Made Simple - YouTube
At around 50min, he shows this slide:
The question is, how many key-value mappings are there? It’s actually really hard. Because there are some that are explicit, like getAttribute()
that returns the value of an attribute given the name of the attribute. And there are other similar ones, like getHeader()
and getProperty()
. But then, if you squint, you see that all the get...
methods are really just keys for their respective values. So getLocalPort()
is just going to return the port. The method is the key, the port is the value. And most of the other stuff is like that. But they’re all ad hoc and different. He then shows how a Ring request map can represent the same stuff but through a unified interface, the map.
Now, to be clear, you lose something. Specifically, you lose static type information. That Java class did encode a lot of static information that we’re now throwing out. And you get static checks on the names of the methods.
But we gain a lot, too. We can iterate over the keys. We can serialize it as we serialize other things. And generally, we know how to operate on hashmaps because it’s common in Clojure. We don’t have to learn any new patterns. It’s at this higher level where a lot of the magic happens. Reuse. Decoupling. Composability. Higher-order constructs.
I think Rich is making a similar claim with Haskell’s types. Many times, you’re just creating ad hoc key-value data containers. And each of them has a name and writes in stone what keys it accepts. You get safety. But if you put them in a map, you’d get the other things.
Clojure separates out defrecords from deftypes. deftypes are for low-level implementation entities. They have a fixed set of fields. These are equivalent to Haskell’s records. But there’s nothing like defrecord in Haskell. There’s nothing to flexibly and consistently represent domain entities. Haskellers use the same construct for both. At first I didn’t understand why Clojure had two things. But after a while I saw that it was a very profound design decision. In the end, though, using hashmaps instead of defrecords won out. It turns out you didn’t need a third thing except in very limited cases.