21 October 2021

'Partial' Predicates in Clojure

Honestly, I'm still on the fence with this one.

Advent of Code 2018, Day 6 was much more involved than the previous 2018 puzzles. There's lots of grid work. Concurrent with my work on this puzzle I was reading the Clojure Style Guide and was struck by the guidance on favoring the partial function over anonymous predicates.

For convenience, I'll post the example here:

;; good
(map #(+ 5 %) (range 1 10))

;; (arguably) better
(map (partial + 5) (range 1 10))

The 'good' example defines an anonymous function with #(+ 5 %), which is equivalent to (fn [x] (+ 5 x)).

The '(arguably) better' example defines an anonymous function with (partial + 5), which is equivalent to (fn [& xs] (apply + 5 xs)).

There are two things I like about the partial approach compared to the #(... %) approach:

  1. There are no special syntactical characters required (such as the overloaded reader dispatch).
  2. It allows for more flexible, multi-collection usage of map as in (map (partial + 5) (range 1 10) (range 1 10)).

There are a few things I don't like about the partial approach:

  1. It's less concise than #(... %)
  2. It's a bit more opaque compared with #(... %) since you have to envision the extra parameter(s) (%) yourself.
  3. This only works when the dynamic argument(s) of the wrapped predicate come last.

I'd like to contrast these two approaches with some of the code from the puzzle at hand. There are two functions being invoked in predicates:

(defn infinite? [landmarks location] #_"implementation omitted")
(defn closest   [landmarks location] #_"implementation omitted")

Here's how it looks with partial predicates:

(defn calculate-areas [arena landmarks]
  (as-> (map (partial closest landmarks) arena) $
        (remove nil? $)
        (map last $)
        (remove (partial infinite? landmarks) $)
        (frequencies $)))

Here's how it looks with #(... %) predicates:

(defn calculate-areas [arena landmarks]
  (as-> (map #(closest landmarks %) arena) $
        (remove nil? $)
        (map last $)
        (remove #(infinite? landmarks %) $)
        (frequencies $)))

I'm on the fence. Which do you prefer?