Author here, I don’t really blog much, but I thought I’d share some brief thoughts about clojure as I’ve been learning it lately. Feedback/complaints/thoughts/etc. welcome
Thanks for the write up! I wouldn’t mind reading more on this topic. I’m a Clojure neophyte myself, but I can definitely see the issue with understanding what shape of data is being sent to a given function
It really just emphasizes the need for good docs. There’s a type-hinting mechanism via the ^ metadata construct, but I’ve found it to be… lacking, and this is coming from someone who’s very used to Python and it’s type-hinting bandage solution to this problem.
What kind of write-ups would you like to see on the topic? I’m by no means an adept author, but I’ll take a stab as I have time and motivation :)
Some things that come to mind:
What do you mean by “keyword types”? Do you mean :foo keywords?
Yeah, a keyword is a type in Clojure (though not every case of a keyword is it’s own type, they’re just identifiers in the same way that variables are).
To be clear, I know what a Clojure keyword is. I care a lot about helping Clojure grow as a language and it’s fun to see folks new to the language talk about their experiences; I think my experience in Clojure has blinded me to the idea you’re pointing at and I want to understand it better.
In the post you said, “They’re like fancy namespaced enumerated types”. and here you’ve said, “not every case of a keyword is it’s own type”. What do you mean by “type”? To me, “type” is about the data type, so from that perspective all keywords are the same type: keyword. And while they’re interned (making them super fast for equality), they can be generated on the fly and aren’t bound to any pre-generated list, which is what I associate “enumerated” with.
Edit: oh I see what you’re talking about. I updated the post to be correct :)
Regarding the issue with the “shape of data”, you may want to have a look at Clojure Spec. But if you ask me, you can skip it and just take the incredible library malli. Not only is it a fantastic schema engine where types are arbitrary predicates instead a narrow selection of hard-coded categories like “string” and much better composability than in e.g. JSON Schema. It is also programatically extensible and has some great features like: generating data pased on a schema, conversion from and to JSON schema, function schemas and so on.
Thank you! I will have to check out malli. I’m not sure specs are exactly the solution that I’m looking for. In many languages, it’s pretty easy to determine the exact return type as it’s declared as part of a function definition. In Lisps, this is not the case, and Clojure is no different in this regard. Functions in Lisps don’t explicitly define a return (via a statement) or return type, which can also change depending on logic. The tradeoffs for this are interesting, because it means the code is less cluttered and potentially easier to just read, but understanding the interface to a function (ie. how to use it, and what it’s used for) effectively requires either good docs or reading and understanding the function itself. Other languages, such as heavily type-hinted Python, Java, C, Go, etc. all have functions explicitly return a value of a defined shape, so just glancing at a function and having a rough understanding of what it does is generally easier. Whether that’s a good thing or not I think is up for debate, there is value in forcing the user of an interface to actually understand the function, but it may slow progress down unnecessarily in cases. This also could be interpreted as a general problem dynamically typed languages have, but I think the implicit return construct makes dealing with it a little worse.
In my experience as a Scheme (and back in the day, Ruby) programmer, it’s not so much the implicit return that makes it difficult to figure out the types, but the way everything is super generic. There’s the sequence abstraction which works for lists, vectors, lazy sequences and even maps and sometimes strings. So if you’re reading a method’s code that only uses these abstract methods, you have no idea what goes in and only a vague idea of what comes out of that method. With Scheme, for example, you’d see (string-ref x 1) and immediately know that argument x must be a string. With Clojure, you’d see (nth x 1) and be none the wiser. Of course, it allows for code that’s more generic so you could use the same code with different types of inputs, but in most many cases that genericness isn’t important and actively hindering your understanding.
(string-ref x 1)
(nth x 1)
Couple this with the practice of using maps everywhere (which can have any ad-hoc set of attributes), it gets pretty murky what’s going on at any point in the code. When you’re reading a method, you have to know what the map looks like, but you don’t know unless you trace it back (or put in a printf). Compare this with user-defined records (which Clojure has but isn’t typically used as much), where the slots are all known in advance and are always present, it’s much easier to read code that operates on them, because whenever a value is extracted, you can derive the type simply from the fact that an accessor method is called.
Malli or spec are a good way to introduce “sync points” in your code at which maps are checked for their exact contents, but I’ve found that more useful for constraint checking at the input/output boundaries. Doesn’t help that much while you’re writing the main code that actually operates on your types. Especially with Malli, I’ve had to remove some internal checks due to performance issues when using validate.
I totally see where you’re coming from. Personally I came from Java when I discovered Clojure and I also sorely missed the type system.
Clojure is much more about abstracting behavior and it actually matters a lot less what exactly the shape of the data is in a certain context as long as you know you have the guarantees you need in your current context. It’s not considered good style to write operations that only work with a super specific data structure. It’s actually the same in Java where this can be done using interfaces.. with the drawback of being limited to one interface at a time. Actually there is something like an inverse of drawbacks of dynamic typing in the static typing world too, and that is the global scope of type names. If you limit yourself to a narrow set of global types, you usually pass around way too much data and/or behavior. The more specific you get, the harder it becomes to properly name things, because you need to differentiate everything from everything and you end up with a zoo of poorly named stuff. Dynamic typing on the other hand allows you to be terse and contextual with the drawback of having to be quite disciplined about making the context understandable.
What I love about malli is that you can actually defer some pretty tedious-to-model logic that would otherwise bloat your code base to the schema engine. Let’s say you have a medical questionnaire where the gender is asked for and if the gender is “female”, then the data should contain the answer to the question “pregant yes/no”. And you need to validate the data in the back-end and update the database. Malli allows you to write a schema in which contextual dependencies between single data points can be captured. Being able to let the schema allows you to write much better code that doesn’t need to know such details. Maybe the actual logic left is then just to apply a JSON merge patch, completely independent of the specifics of the data at hand.
I remember trying Clojure a bit, and being super interested in a lot of the ideas of the language.
There is the universal quibbles about syntax (and honestly I do kinda agree that f(x, y) and (f x y) are not really much different, and I like the removal of commas). But trying to write some non-trivial programs in Clojure/script made me realize that my quibble with lisps and some functional languages is name bindings.
(f x y)
The fact that name bindings require indentation really messes with readability. I understand the sort of… theoretical underpinning of this, and some people will argue that it’s better, but when you’re working with a relatively iterative process, being able to reserve indentation for loops and other blocks (instead of “OK from this point forward this value is named foo”) is nice!
It feels silly but I think it’s important, because people already are pretty lazy about giving things good names, so any added friction is going to make written code harder to read.
(Clojure-specific whine: something about all the clojure tooling feels super brittle. Lots of inscrutable errors for beginners that could probably be mangled into something nicer. I of course hit these and also didn’t fix them, though…)
EDIT: OTOH Clojure-specific stuff for data types is very very nice. Really love the readability improvements from there
Interesting to hear this–indentation to indicate binding scope is one of the things I really miss when I’m using a non-Lisp. I feel like the mental overhead of trying to figure out where something is bound and where it’s not is much higher.
(I strongly agree on the state of Clojure tooling.)
I think that racket solves this:
(define (f x)
(define y (* 10 x))
(printf "~a ~a\n" y x))