To comp or not to comp

I definitely find

(filter (comp #{:oolong} :type) teas)

easier to read (and especially to skim-read) than

(filter #(= :oolong (:type %)) teas)

not least because with comp I know that the data flows predictably right to left, whereas with the anonymous function one can make the data flow arbitrarily complex.

With simple examples like this it’s not much of a difference, of course, but in general, to me, #(...) means “stop and read carefully” whereas (comp ...) is closer to “does what the function names say on the tin”.

8 Likes

Nice. I like to use sets instead of an anonymous function equality check. But aren’t there performance overheads for effectively doing contains? Rather than equality? Or is the overhead of an anonymous function greater?

Honestly I don’t know, I haven’t measured it. It’s quite likely its slower, but does it matter?

There are circumstances where every microsecond counts, and in that case you should measure everything and use your time budget wisely. Have a look at @ikitommi’s talk from ClojuTRE this year for a good example of that.

For most of what I do day to day the difference between (= :foo bar) and (#{:foo} bar) is not going to make the difference.

1 Like

That’s interesting, because I’m completely the opposite. I’m pretty comfortable in Clojure, but I almost never use comp, I always prefer anonymous functions. In your case, the second version reads naturally to me: I’m looking for items whose :type value is equal to :oolong, and that’s literally exactly what the code says. You’re right that this is a short example, but I generally find the readability of the function composition operators (comp, partial and the like) goes down with larger uses, not up.

I generally shy away from using collections or keywords as fns as well, since IMO it’s easy to obscure what’s going on. I almost always prefer explicit use of get and contains?.

4 Likes

Interesting, I don’t know what I’d do if keywords and collections weren’t functions. Can’t remember the last time I used get or contains?. :sweat_smile:

I do try to minimize my use of comp outside of transducers just because of the context switch of having to read backwards. In a language like javascript where there isn’t a threading macro and most of the functions take the collection last (in FP libraries) I use the equivalent of partial and comp quite heavily.

Yes, I agree about comp and pretty much only use it in transducers too.

I probably overstated the case for keyword-as-fn calls, now that I think about it, I definitely use things like (map :order-id orders) and would probably use (:name user) too, although I often use get, particularly with nested structures. I definitely prefer contains? though, and I very rarely use collections as fns.

1 Like

This post has sparked some interesting discussion, here and elsewhere, which is great. I’m certainly not claiming this is the best or only or even recommended way to write this code. I do think however that it’s a good and valid way to write it.

Some of the arguments (mostly elsewhere) have mostly been against the overuse of comp and its ilk. I think that’s valid, just like you don’t want to write Java-in-Clojure, you also shouldn’t try to write Haskell-in-Clojure.

I mostly use comp/juxt/partial and friends in specific idioms, like the one I described in this post. As I explained in an earlier post I think idioms like these are really useful because to the reader familiar with them they scan really easily. Your brain is great at pattern matching like this. But use of idioms (sense 1) is personal and depends on your own idiom (sense 2) and that of your audience/peer group (readers, team members).

After becoming more familiar with comp and friends through use of these patterns I started seeing other places where it would come in handy. It has also led me to write functions that are more amenable to comp-ing and partial-ing, and I think that’s a very positive evolution in my development as a programmer. I think this has improved my code and the systems I create.

There is a small but vocal section of the community that seems to argue actively against the use of these functions, and that I find regrettable. They are great and useful tools. They may take a bit of getting used to, but that effort is so worth it. Get out of your bloody comfort zone. If (some #{element} collection) can be mainstream and composing transducers with comp can be standard practice then surely we can consider (comp #{:foo} bar) as a valid idiom as well (whether it’s widely adopted or not).

As always use your own good taste and judgement, accept that not everyone does things the same way, and above all be kind to each other.

8 Likes

@plexus Firstly thanks for sharing these idioms, your advent of blog posts has been awesome! I’m really enjoying it. :smiley:

Back on topic:

It’s funny when I write javascript I do go out of my way to write things that support partial and comp. In clojure not as much. I think this is because I’m most of the time I’m more interested in getting things to like up with ->> and -> for better or worse. As much as I’m comfortable with using comp I think:

#(-> % bar #{:foo}) is more intuitive than (comp #{:foo} bar) to people with less experience with FP. Or even better (fn [x] (-> x bar #{foo})).

Same goes for:

(map (fn [[k v]] k (inc v)) coll) vs (map (juxt key (comp inc val) coll)

I mean I can read both and I’m fine with either, but I don’t work alone and try to keep in mind how things read for people who are new to the language and paradigm. If clojure didn’t have threading macros I’d use comp everywhere like I do in javascript though (so I’m not going to drop everything just to make things easier for newcomers).

As for partial and juxt I use them where it makes sense and find them very handy. Personally, I prefer (partial foo bar) but think #(foo bar %) is probably easier to parse for someone new to the language, but again I don’t mind either.

As for collections as functions I find it too cool not to use. The first time I saw stuff like:

(keep {:a 1 :b 2} coll) I was hooked. :smiley:

But like you said ultimately its mostly down to personal preference and context.

I mean what I’m really against is using threading macros! We should just write things back to front the way lisp has always been written:

(reduce + (filter #{1} (map inc (map dec [1 2 3 4 5]))))

If it was good enough for McCarthy why not us? :stuck_out_tongue:

It really comes down to what you’re used to. #(-> % bar #{:foo}) definitely gives me pause. The threading macro plus the lambda is not a combination I’m used to seeing. (comp #{:foo} bar) is just something I immediately recognize.

This is even more pronounced in

In the second form (map (juxt key val) coll) is such an ingrained pattern that I immediately see that the only thing that’s really happening here is inc. The first one takes actual time to read and process (for me).

3 Likes

I will not use comp to composite simple functions, like (comp #{:a} :type). Lambda can do the same without losing readability. But I can use comp to composite high order functions.

(fn [x] ...) is a function.
(fn [opts] ... (fn [x] ...)) is a function with a constructor.
(fn [opts] ... (fn [next-hdlr] (fn [x] ...))) is a composable function with a consturctor.

Then I can do

(def task 
  (comp (job-1 some-opts)
        (job-2 some-opts)
        (job-3 some-opts)
        identity))

I learn this from boot.

5 Likes

Whoa – that is some nice higher-order function work! This is clearest use of comp that I’ve seen, as opposed to using a threading macro.

Personally (not trying to write Haskell, because I don’t know Haskell; and not speaking to transducers because I haven’t really mastered them yet), I enjoy writing my code with comp; when I use threading-macros my focus as a writer is on data transformations; when I write comp, my focus is on function transformations. I have a different feel for what it is I’m writing, and while I’m writing I rarely find myself wondering which to use; if the thing i’m figuring out is which functions will work I write comp. If the functions are givens and it’s just mechanically manipulating the data, it goes in threading/lambda.

4 Likes

I think the more important question is not whether you should use comp, but whether you should learn comp and be familiar with it, and that’s a resounding yes!

And since that’s a resounding yes, then using comp is not an issue. So I say do use comp, and do use anonymous functions, and it doesn’t really matter which one you use and when. Use them both, don’t even need to be consistent in your usage.

As far as I know, there’s no real downsides to comp over anonymous or anonymous over comp. Which is why this is a case just like for vs map. Personal preference and specific situations might lean you towards favouring one over another. As a Clojure dev, you are expected to know both just as well.

4 Likes

I find comp and transducers achieve a sought-after goal: expressing composition in an immediately recognizable form. There are other ways to compose, but these two are narrowly focused tools. And once you are comfortable using them, their presence instantly conveys “composition” and parameter signature(s).

Threading macros achieve almost the same goal, but with more variability in the parameter arity signatures.

5 Likes

This part I agree heavily with. I think the same applies to map > for in e.g. JavaScript. If you have a narrower function that does the same job, readability goes up if you use the narrower function. It is possible to hide caveats in #(-> ...), while (comp ...) is explicit about what it does.

I do however feel that the right-to-leftness of comp takes a little getting used to. I’m not using it as much as I ought to. I use partial quite a lot though. I feel it’s really useful for the “pattern” of defining generic functions, then creating more specific versions of the functions further down.

5 Likes

This is a better way of saying what I was trying to convey above. Thanks.

3 Likes

I had used comp a lot in the past, but read somewhere that it emits a huge amount of JS from cljs (compared to an anonymous function). Partial was declared fine.

Does anybody know if this is true? And does it matter after advanced compilation and gzipping?

1 Like

The argument that we should avoid useful language features because they’re unfamiliar to people who don’t have Clojure experience is very strange to me. Many of the best parts of Clojure are unfamiliar to people coming from a range of other languages. We should seek to help newcomers come up to speed on the language, not to hide the good stuff for fear of confusing them.

5 Likes

i have given this some thought… and… well… i went back and forth on whether or not i should comment on this… since well… basically i think that you are wrong :smile:… ( at least in part )… and i really would hate to touch any wrong nerves… certainly… but what’s the harm in a little debate?.. (…ehh… i typed one ? and … there… so why does it display ?.. in the preview … ? … hmmm… well anyway… )

sooo… in any case… first off… i do agree with most of what you said… people should learn / know about various language features… for, if it was otherwise, how could they even begin to form something like an informed personal preference…

what i want to object to is really this:

now why is that?.. well… i think i can agree with this BUT!!!.. ( exclamation mark and dots are weird too… really… what is this ?!.. some sort of escaping mechanism?.. anyway… )… BUT!!! only in the context of relatively small projects… with not more than a handful of people working on it… lets say… BECAUSE… when you are working on a large project… spanning many years…with a handsome team… well… it just becomes a real pain if everyone opts to use this or that particular style / feature for no good reason other than personal preference… since READABILITY etc. are really gonna suffer if each time a new coder joins the team she introduces her own formatting and looping style… (… in java there are a huge number of ways to traverse / loop over a collection… mainly because of the fact that the language has changed so much over the decades… and i am not saying that you should never adopt / mix with new features… for i’d consider that a valid / good reason… ). what i want to speak up against is the programmer who is bored by her day to day job and wants to spice things up by making use of a more exotic feature… in a situation where there are perhaps tons and tons of examples done by colleagues in a more conventional style… which has done the job to the customers content for years and years… so just copy paste :smile:… follow the established patterns!.. do not try to reinvent the wheel!

so what i would suggest is this… study about a language as much as you possibly can… obviously indulge also in learning by doing… when you write small helper programs amounting to perhaps a couple of hundred lines or so… well… by all means… go all out… do your custom macros, do destructuring, do tacit / point-free programming… WHATEVER!!!.. that’s great!.. because that’s how you learn!.. BUT!!!.. when it comes to those projects, where the rubber really hits the road… well… then i absolutely believe that having a code-base that is consistent in how things are formatted, commented… in the idioms that are being used… that can really really make a / the difference… surely… as the years go by this will become ever more apparent!..

IN FACT!!!.. i would like to argue, that tooling such as:

can be a great asset to a large project’s success!!!.. make them part of your CI pipeline… someone violates the whitespace conventions… HAVE THE BUILD FAIL!!!.. i know… this sounds ridiculous… but… and again… i am truly convinced, that small stuff like that really does matter!!! (…also i would love to see some of those tools improve even more over time… )

hmm… so…“de gustibus non est disputandum”… well… that’s latin… so that alone should tell you that its out of date :smile:

iiiiin any case… i have again written much more, than i would have liked to… also… as i wrote this… and as i have come to know you over the past few months… i guess… i may just be kicking in open doors here… so enough of this already!

Does anybody know if this is true?

I recall something like that - but with partial being the villain against the “simpler” #()

A quick test didn’t seem to show drastic differences between comp and partial on the js output (regardless of the optimisation level).
Perhaps it’s no longer the case?