← Back to context

Comment by michaelteter

2 months ago

The first thing I would do would be to take some of the inner forms and turn them into functions (single responsiblity). This isn't even Clojure-specific.

Lots of little, simple functions, composed... You can still use the threading at a higher function, but instead of doing the actual work at each step in the pipeline, you call a well-named function which does what that step was doing. Yes, it may feel redundant, but it absolutely improves human comprehension because it reads like clear human instructions.

Tests become much simpler too.

The only two challenges I face with this approach is 1 - naming things, and 2 - organizing. The function names tend to become quite long, since they ideally describe what the function does.

I haven't done this lately, now that the AI tools have become pretty great; I can imagine that an AI tool would be an excellent knowledge base for these many functions, and it would probably be very successful in providing additions or modifications given the clean, clear function names and brief contents.

As I gained more experience writing Clojure, I went in the other direction; familiarity with the standard library means that any composition of those functions and data structures is easily grokked. People's strained attempt to name every little line of code? Not so much. You end up jumping around the code a lot and, in my case, suffer the "portal effect" where you forget the context of what you were trying to understand. Also, namespaces become polluted with functions that only serve as assistants to your main function. This is a lesser concern, but still, it dilutes the focus of the namespace.

A nice middle way is using let bindings for whatever you want to give clearer names. You get the name you want while reducing the reader's scope to the literal let binding.

I do agree that big functions can be unwieldy, but I think that if it's mostly core clojure fns, and you're experienced, much bigger sized fns are acceptable than name-everything advocates generally prefer.

But it's individual and an artful balance even if the only reader is yourself. There's no silver bullet.

>Yes, it may feel redundant, but it absolutely improves human comprehension because it reads like clear human instructions.

That should always be how code is written, including inside of functions, which is the real problem here. Something absent from the original code that is needed in a case like this isn't more functions, it's comments and good names.

Pulling something out into a function that is purely individual application logic without intent for reuse is not the right way to use a function, whose purpose is to be called, ideally somewhere else and not just once. As Ousterhout points out in his book, proper abstractions hide information and implementation, they aren't just scaffolding that increase the surface of an interface without doing anything.