Abstract Clojure

Abstract Clojure


·November 25, 2021

asdfasgasdgasdg · 10 days ago

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.

dwohnitmok · 9 days ago

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).

d3nj4l · 10 days ago

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.

deobald · 9 days ago

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.

amackera · 10 days ago

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!).

whalesalad · 9 days ago

Thank you for this rec, been thirsty for knowledge in this domain.

capableweb · 9 days ago

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

niels_bom · 9 days ago

Great book. Great podcast by him as well: https://lispcast.com/category/podcast/

mark_l_watson · 10 days ago

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.

charesjrdan · 10 days ago

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.

Though I don’t use Clojure professionally, I’ve definitely become a better developer just from playing with it, a contrast to JavaScript where I feel like learning it actually lowered my iq…

didibus · 9 days ago

> 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.

erik_seaberg · 9 days ago

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.

fault1 · 9 days ago

java 1.0 most definitely had interfaces.

smalltalk on the other hand, did not have explicit interfaces: http://www.jot.fm/issues/issue_2002_05/article1/

didibus · 8 days ago

Hum, guess you're right. For some reason I thought they introduced them in 1.3.

pjmlp · 9 days ago

The same Java that took the interfaces idea out of Objective-C protocols on version 1.0?

Zababa · 10 days ago

> 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.

147 · 10 days ago

Doesn’t it also make the stack traces more difficult to read as well?

slotrans · 9 days ago

"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.

WJW · 9 days ago

That really sprung out at me too. I don't think the problem is well understood at all, let alone the answers to it.

didibus · 8 days ago

I'd have to respectfully disagree with the article. It seems like unnecessary abstraction that just obscure the logic, and it makes the code much harder to reason about in my opinion. There's this implicit behavior injected which may or may not be pure.

Here's my suggestion instead, break out your pure and impure behavior. Don't design it so that you inject the behavior, instead seperate them into independent units and compose them at the handler.

    (defn get-article-id
      (get-in request [:path-params :id]))

    (defn make-response
      [status body]
      {:status status
       :body body})

    (defn get-article!
      [data-source request]
      (->> request
           (db/get-article-by-id! data-source)
           (make-response 200)))
This is often known as the Functional Core, Imperative Shell pattern. The functional core cannot call out to the imperative shell.

grzm · 8 days ago

Design is about tradeoffs, as I think something the submission addresses very well. Something I recognize about your implementation of get-article! is that it knows a number of implementation details:

* it handles http requests * it uses a jdbc data source * the result of the parsing function and the arguments of the database fetch function are coupled, * the result of the database fetch and the formation of the http response are coupled

This has implications both in testing and in application maintenance. What needs to change if you decide you want to add caching in front of the database? What if you want to pick up requests off of a Kafka stream instead of from a web server?

Like I said, there are tradeoffs. The entire premise of the article is about depending on abstractions rather than implementations. Depending on your particular context, where you want to end up on the continuum should reflect your particular context.

Blackthorn · 9 days ago

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.

potency · 9 days ago

What is Clojure's main selling point?

joelittlejohn · 9 days ago

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.

fulafel · 9 days ago

There's also transcripts of this and other Rich Hickey talks available: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...

beders · 9 days ago

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.

vnorilo · 9 days ago

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.

fulafel · 9 days ago

It's very good and straight to the point about transforming data. The central data structures (maps, keywords, vectors) and excellent languge + standard library centered around those, combined with interactive iteration at the REPL, make for a rewarding development experience when you can just try stuff. Since most things are immutable and passed around as data, you can easily rerun things at the REPL with high confidence that objects in your data aren't being mutated.

ByteJockey · 9 days ago

It's lisp on the jvm.

Scarbutt · 9 days ago

Not writing Java (although you do have to do lots of interop with Java).

capableweb · 9 days ago

It's true it's "not writing Java", that's true for every language besides Java! But the second part is definitely not true. First, you can very much use JVM Clojure without touching Java, I've done so many times. Secondly, you can use ClojureScript which cannot even do interop with Java since it "compiles" to JavaScript and doesn't run on the JVM. Thirdly, you can use Babashka to run Clojure code with GraalVM and SCI instead.

Many options exists to not having to touch Java when you use Clojure, but I guess it's hard to kill old memes?

Scarbutt · 9 days ago

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.

fulafel · 9 days ago

I've seldom had uses for Java interop, once in a blue moon. An exception is FP math functions and constants, using those from java.Math via interop instead of having a native math library has been the Clojure way (but seems getting a native wrapper in Clojure 1.11, clojure.java.math).

pjmlp · 9 days ago

Then using the JVM to start with isn't really the cleverest decision.