I'm not sure I agree here. There's not too much value in testing these two or three lines of code in the get-article handler. And the passing around of compositions of higher order functions can get confusing. There are cases where an approach like this is warranted but I would say that there needs to be a significant level of complexity before this makes sense.
Having a protocol for your data layer and an in memory implementation of that for testing can make sense, especially when your real data store is expensive to bring up, but I would try to minimize the number of seams like this in my system. Each one introduces cognitive overhead due to indirection, so you should use them judiciously.
Yes Clojure is all about first-order functions working on simple, concrete data structures.
Pervasive use of protocols and custom, user-defined higher-order functions I would argue is unidiomatic Clojure and creates long-term pain as you end up with opaque functions being passed around that can't be inspected at the REPL and a lot of tricky "fitting functions together" that is made difficult without a static type system. You sometimes need them, but you should reach for them very judiciously.
There's a reason Clojure emphasizes data over functions (and both over macros).
Yes - sometimes you can go too far in the quest for abstraction. I've seen a ton of over-enterprisey people build gorgeous abstractions with perfect testability and dependency management, only for it to be used only ever in one context in one way. I lean towards WET first before extracting an abstraction, if that.
The argument for pure functions holds in a real system; these particular signatures will be familiar to anyone who's built a few Clojure web services. In reality, some of the functions listed (say, `get-article`) would disappear entirely into a system-specific convention. There's a balance to be had between the number of seams the team is managing directly and those which, by their creation, mean less cognitive overhead. The seams then exist only if they are required, as certain handlers might choose to diverge from the conventional functions used in a generic way.
It's of course not easy to see that next step from the article, since it doesn't eliminate any code by creating pure functions. But even in a toy example, there is value of creating pure functional abstractions. In some codebases, you might even see the team lead segregate pure functions by namespace: "Pure stuff over here, tainted stuff over there." In those situations, teams try to reduce impure surface area -- in this case, anything that touches the `db` namespace.
I also recommend the book "Grokking Simplicity" by Eric Normand for a longer exploration of functional software design (not Clojure-specific). The linked blog post uses Clojure examples, but this approach to software design is universally applicable (especially in functional programming!).
Thank you for this rec, been thirsty for knowledge in this domain.
Another good one is "Elements of Clojure" (https://elementsofclojure.com/) which I think is a slightly misleading title. It's a generally good programming book, it just happens to use Clojure for it's examples, but I don't think it's required to know Clojure to understand the concepts explained in it. Also been discussed here before: https://news.ycombinator.com/item?id=21090288 and https://news.ycombinator.com/item?id=11306519
Great book. Great podcast by him as well: https://lispcast.com/category/podcast/
Nice article. I have to admit living in the dark ages, Clojure wise. I don’t even use Protocols, just simple functions and I love the simple built in data structures. I was comparing my Clojure and Common Lisp libraries for using OpenAI’s GPT3 APIs last night. I usually use CL, but I notice how much cleaner the Clojure version looked (I should refactor the CL version).
Clojure is such a practical language.
Great pointers, I started learning Clojure for a hobby project and was initially put off by the lack of ‘frameworks’ like nextjs or rails, but after effectively piecing my own stack together from components like Reitit and Integrant I’m really glad I didn’t use a framework.
I actually feel like I understand everything that happens in the system now, and when problems arise I can REPL in and hunt them down with confidence.
> The protocol is another form of abstraction we can use to decouple modules, the approach is more object-oriented than functional
That's not true that the approach is more OO.
People get confused because a lot of OO languages like Java eventually added support for something protocol-like, in Java it was interfaces for example, but for a long time Java, an OO language, didn't have interfaces, you only had Classes and Objects and inheritance.
Protocols and polymorphism is not an OO concept, and doesn't even need to involve Objects at all.
In Clojure for example, you can dispatch the protocol over a map, given two maps based on their metadata the protocol will pick a different implementation for the called protocol function. There are no Objects and Classes involved.
All you need for polymorphism is a sort of metadata over a datatype, it could be type information or it could be something else, like attached metadata like in Clojure.
In an OO language, and in Clojure by virtue of running on the JVM, user datatypes are defined using Classes and Objects, but in a language like Haskell they're not.
So basically you can implement protocol-like polymorphism, basically the idea that you dispatch based on meta-information about the arguments passed to the function with or without object constructs.
I'm pointing this out because it is an argument against OO. The fact that even in most OO language people have over time preferred to use such polymorphism over object inheritance hierarchies is a sign that Objects aren't as useful as ounce thought.
What is very useful though is to be able to define alternate function implementations based on some metainfo about a given argument, such as their type. So much so that all languages, OO and Functional will tend to have such feature.
Are you sure about Java? http://titanium.cs.berkeley.edu/doc/java-langspec-1.0/ talked about interfaces in 1996 and I don’t remember any version that didn’t have them.
"Software design is a well researched and understood problem..."
I got a good laugh out of that one. Honestly that final paragraph should just be deleted. It's just a giant, indefensible claim.
That really sprung out at me too. I don't think the problem is well understood at all, let alone the answers to it.
Reddit conversation here: https://www.reddit.com/r/Clojure/comments/r1x1ji/juxt_blog_a...
If you use Integrant like they suggest, you don't need to do any of that for testability. You can just use the code you had originally and have a test function that just creates an Integrant system with all fake dependencies. Then you can just reuse that test function in every test, occasionally overriding one of the fake dependencies.
What is Clojure's main selling point?
If you have an hour spare, probably the best way to understand Clojure's main selling points is to watch this talk: https://www.infoq.com/presentations/Simple-Made-Easy/
InfoQ list the Key Takeaways as:
- We should aim for simplicity because simplicity is a prerequisite for reliability.
- Simple is often erroneously mistaken for easy. "Easy" means "to be at hand", "to be approachable". "Simple" is the opposite of "complex" which means "being intertwined", "being tied together". Simple != easy.
- What matters in software is: does the software do what is supposed to do? Is it of high quality? Can we rely on it? Can problems be fixed along the way? Can requirements change over time? The answers to these questions is what matters in writing software not the look and feel of the experience writing the code or the cultural implications of it.
- The benefits of simplicity are: ease of understanding, ease of change, ease of debugging, flexibility.
- Complex constructs: State, Object, Methods, Syntax, Inheritance, Switch/matching, Vars, Imperative loops, Actors, ORM, Conditionals.
- Simple constructs: Values, Functions, Namespaces, Data, Polymorphism, Managed refs, Set functions, Queues, Declarative data manipulation, Rules, Consistency.
- Build simple systems by: Abstracting (design by answering questions related to what, who, when, where, why, and how); Choosing constructs that generate simple artifacts; Simplifying by encapsulation.
So Clojure is a language that embodies these principles in its design. It's a Lisp, which means that all code is constructed from a very regular expression syntax that has an inherent simplicity and can be quickly understood. It's a functional programming language that provides exceptional tools for minimising mutating state, and it favours working with a small set of data structures and provides a core api with many useful functions that operate on them.
I'd say the result is getting a lot done with a small amount of code, minimal ceremony, true reuse, and the ability to maintain simplicity even as your system's capabilities grow.
A really well thought out Lisp-1 that runs on the JVM, in the browser, in node as well as the CLR and BEAM.
It is hard to go back to other languages once you appreciate its simplicity.
Sibling provided a good overview of philosophy. Practically it offers Lisp goodness, jvm/js interop and a well-designed set of persistent collections everyone and their dog uses, part of the reason why clj libraries tend to compose very well. You get some stunning mileage out of the thing.
It's lisp on the jvm.
Not writing Java (although you do have to do lots of interop with Java).
Many options exists to not having to touch Java when you use Clojure, but I guess it's hard to kill old memes?
If you are writing toy programs in your mom's basement sure but every real world clojure project I have work with had to use Java libraries.
When I said 'not writing Java' I was obviously talking about the JVM.
> One of the trade-offs with this approach is that it results in additional wiring; functions must be passed their dependencies and wired together to form a system. The indirection means we can no longer jump to the definition of get-article-by-id in the server namespace.
That seems like a huge loss. I feel like that approach could be great for business logic maybe? There's the "business logic as a library" technique that works well for this, and allows for easy testing.
Doesn’t it also make the stack traces more difficult to read as well?
Rough to read Clojure code without never using it.