To comp or not to comp

At least in my case, that’s not my argument. I’m familiar with them and I have a lot of Clojure experience, but I don’t use them because I think the code is less clear with them and the alternatives are more readable.

That’s… excessively harsh. I have a lot of Clojure experience, and as I wrote above, I know how they work and I prefer not to use them. IIRC Alex Miller also commented that he prefers #(...) to comp and partial, and I’m pretty sure he also knows how they work. It’s just a style difference, and not everyone will agree with the way you like to do things.

3 Likes

I’m pretty sure I’ve seen Rich suggest #(...) instead of partial a few times too. He may even have said that if he were doing Clojure over again, he probably wouldn’t add partial in the first place.

I don’t recall him mentioning comp in any negative way and of course now it is idiomatic for transducer usage, regardless of other uses.

When I first got started with Clojure, back in 2010, I was pretty enamored with partial and comp but I have backed way off on them over the years, preferring to work with #(...) and (fn [...] ...) and the threading macros as needed.

Here’s an early piece of code from work that mixes partial and comp and I certainly would not write it this way today:

(defn value-list
  "Given a column name, return a function that extracts that
   column's values from a resultset and return them as a list."
  [col]
  (comp (partial str/join ",") (partial map (keyword col))))

Even with close to nine years of Clojure under my belt now, I still look at this and squint and have to actually think about what that code does. Today, I would probably write that body as:

(fn [rs]
  (->> rs (map (keyword col)) (str/join ",")))

or even just:

(fn [rs] (str/join "," (map (keyword col) rs)))
3 Likes

That’s true, although it’s a very weird use of comp. I doubt many people are actually thinking about what it’s doing for transducer usage, it’s more “put what I would normally put in ->> into comp instead”.

I totally agree about your code snippet, I’d have to think pretty hard about that to be really sure I understood it. I also like your suggested improvements much more.

2 Likes

Ok, but that’s not a tottaly fair example. You’re just using too much higher order functions in the first, and I have to agree that I see a lot of people starting trying too hard to make everything into higher order functions, and get enamored with partial and comp, also you threw in partial into the mix :stuck_out_tongue:

I like the answer I saw Alex Miller give on a similar question once on the Clojure slack, which I paraphrase:

Avoid it until it makes sense.

And this is where I believe it is something people should learn and become at ease with, because there are times when I do use comp and others when I don’t, and it is intuitive. Sometimes comp just makes more sense for some reason.

Transducers is a really good example, compare:

(into []
      #(((map inc)
         (filter even?)) %)
      [1 2 3 4])
;; => [2 4]

With:

(into []
      (comp (map inc)
            (filter even?))
      [1 2 3 4])
;; => [2 4]

Another good use of comp sometimes is inside of a #() since you can’t nest #(), only for simple cases where nesting fn inside is actually uglier than comp.

I also find generally comp is quite nice when used with unary functions, such as those from clojure.string:

(require '[clojure.string :as str])
(map (comp str/capitalize str/trim) ["hello " " jonny "])
(map #(-> % str/trim str/capitalize) ["hello " " jonny "])
(map #(str/capitalize (str/trim %)) ["hello " " jonny "])

I guess I find the comp above the most readable of the three.

1 Like

Proposed principle:

It is acceptable to use comp when its arguments are simple

Good:

(into []
      (comp (map inc)
            (filter even?))
      [1 2 3 4])

(map (comp f g h) xs)

Bad:

;; pretending we don't have 2-arity map and filter
;; we wouldn't be wanting to use comp
(comp (partial filter even?)
      (partial (map inc)))

(comp #(f 1 2 :something %)
      #(* 2 (+ 10 %))

I’m getting the impression that we want to “avoid too much mess inside comp’s arguments”. For instance avoiding local variable bindings in fn and #(,,, %).

Personal note: I find I appreciate this discussion. I’ve found Clojure code that’s totally unreadable, and too much complex stuff nested inside complex stuff is often the sinner. This seems like a special case: too much complex stuff inside comp arguments that make us miss the point.

Sounds reasonable? Am I missing any cases?

I guess for me it’s: use whichever is more readable and most appropriate for the given situation.

I think the alternative is to suggest there is a Clojure - The good parts, and Clojure - The bad parts. And, maybe there is, but I tend to feel like it is all mostly good, it is just not all good in all situations, and possibly partly a matter of personal preference as well. I’ve seen some people prefer partial for example, since it is clear on the intent, where as with #() it could be doing anything.

And I don’t know, maybe that touches upon something else, what from the Zen of Python is:

There should be one—and preferably only one—obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.

By the way, I quite like the Zen of Python: Zen of Python - Wikipedia

But I’ve always wondered about that particular bit. I think I disagree a little, I like having multiple ways to do something which I can pick and choose from and therefore apply what I prefer or think is best for any given situation. At the same time, having an obvious way, given you don’t know better, is also a good thing. So maybe I’d say:

There should be at least one obvious way to do it.

And in that respect, I would say in Clojure, we might want to make that way be the use of anonymous functions: #() and fn. Where as I feel maybe some of the older tutorials and guides, it might be they give the impression that comp and partial are more obvious, probably from trying to teach FP and getting overly excited about higher order functions :stuck_out_tongue:

1 Like

For what it’s worth, and despite my advocacy for HOFs, the third option is the one I like best here.

1 Like

Re: Python and “one obvious way” – When I learned Python in 2013, that was definitely something that appealed to me. It was a huge relief after learning Ruby a year or two earlier and discovering that Ruby seems to intentionally support ten different ways to do absolutely everything :frowning:

For context, prior to picking up Clojure (in 2010), I’d been doing Scala and I had run into the “split brain” that is the Scala world of a) the Haskell-on-the-JVM crowd vs b) the better-Java crowd – two diametrically-opposed ways of doing anything. So Clojure seemed much more opinionated (which I liked) although it still – back then at least – seemed to have its more “functionally pure” crowd and its pragmatic, “just make stuff” crowd. And clearly my comp/partial monstrosity fell into the former camp. But I think a lot of that FP crowd left in those early years and with today’s pragmatic, maker-focused atmosphere, I hardly ever reach for partial and I’m much more sparing with comp too. I mean, who wants to read all that point-free code anyway?

3 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.