Okay, I’ll bite.
I think it is useful to look at the type signature for something like clmap
that @billburcham mentioned. That article definitely shows the mindset of the statically typed (Church types) crowd. Clojure data is untyped, so obviously it would all be one big algebraic data type.
If you were going to go all the way with Clojure, functions are a valid data type, so you actually need a case for functions in your EDN
type. The clmap
type would then be:
clmap :: EDN -> EDN -> EDN
How absurd this type is! What’s more, nearly every function’s type signature would look like this. There’s no information in it except the number of arguments. And that’s true from a certain perspective. This is exactly as much static type safety we have in Clojure–none.
However, we, as Clojure programmers, know that not all EDN
values are valid for the arguments to map
. We know it, even if it’s not written down. Even if no checker is watching over our shoulder. We know that the first argument should be a function. We know the second argument should be something you can call seq
on. And we know it will return a seq.
But the article does not acknowledge that. It just says “look at this silly type” to dismiss Clojure. Such an obvious straw man. The challenge is to do what Clojure does but in a Haskelly way, not to do what Clojure does in Haskell. Why use Haskell at all if you’re going to throw out type safety?
So let’s start to build out the types. The mental types for Clojure’s map
are very similar to Haskell’s, but perhaps a little richer.
clmap :: Seqable L => (a -> b) -> L a -> Seq b
It’s something like that, except it’s only as strict as you want to be. That is, a
and b
can be precise types that a Haskell compiler would understand, but that’s up to you. You could do anything you wanted. That’s actually a huge difference between the static and dynamic views. In static, what you can do is primary. In dynamic, what you actually do is primary. For instance, you could say that a
doesn’t have to be any particular type, just that the values in the Seqable
are printable. The Seqable
could be heterogeneous, but that’s okay because we’re only going to call print
on them. It’s not possible to do that in Haskell without building a new type and all the ceremony that entails. To me, though, it seems perfectly reasonable to build a List of printable stuff to print out later. Making that possible seems like a good thing and I have ideas about how to make it possible in Haskell. That would give you the type safety of Haskell with some of the flexibility of Clojure.
Let’s look at Clojure’s hash maps. When we use maps in Clojure, there are a few possible scenarios.
-
We know (or assume to know) that the map contains some known keys and potentially others that are unknown.
In this case we treat the hashmap like an “entity”. We can dig values out and update them, leaving the keys/values we don’t care about in place.
-
We don’t know what keys are in it.
We can treat the map as a generic container of key-value pairs. That means we can iterate over the keys, merge two of them, etc.
-
We don’t know the keys and values, but they’re of homogeneous types.
This is like the index pattern, where you’re using the hashmap not as an entity record but to provide a constant-time lookup for values based on an index. group-by
and frequencies
provide hashmaps like this, and it’s common to build them yourself. Think of a hashmap used to look up something by name.
Hashmaps in Haskell are really only meant for #3, where things have easy types (Hashmap String Int). In Haskell, they use data types to represent entities like #1, but those have fixed keys. And you can’t do #2 with them, that is, treat them as generic containers. I know people mention Row Types. That’s great! I think they’re really interesting. But it’s just one feature that kind of solves for case #1 – it lets you talk about minimum subsets of keys and treat them somewhat generically. But #2 can’t be solved by them.
When I say “Row Types are just one feature”, what I’m trying to get at is the challenges of “situated software” require a wholistic view on things. Row Types could play a significant role in a proposed design solution. But there are countless other decisions that need to be made. It’s like saying you can use Immutable.js in your JavaScript, so why use ClojureScript? Sure you can use it, but is it integrated in? Does it cause more impedance mismatch than it solves?
You can’t point at Row Types as an answer and then give your examples in Haskell because Haskell doesn’t have Row Types. You can’t point to a bunch of features and tell me to put them together however I want. That’s a bit like someone asking what web framework to use in Clojure and I tell them to put together a bunch of libraries themselves. It might be how we do it, but it’s not a satisfying answer to the question.
If the existing tools of Haskell (or any language) do give you what you need to write “situated programs”, then where are the guides for how best to do that? Or if they are cumbersome to use, perhaps there are best practices around how to reduce the cumber. Clojure isn’t perfect and we have to do a lot of the same. Or maybe people know it’s possible but they haven’t quite found out how yet. Just be honest and say that. I mean, Rich Hickey really did just give that talk. No pressure to answer right away. Or maybe the whole enterprise of “situated programs” is not relevant. But they’re not saying that, either.
Eric