Thinking Clojure from an OOP Perspective

2025-03-2212:2640satisologie.substack.com

Building an intuition for the essential Clojure functions, transitioning from OOP to Functional thinking

In Object-Oriented Programming (e.g., Java), you’re used to:

  • Objects: Bundles of state and behavior (e.g., class List { void add(item); }).

  • Loops: Iterating with for or while, mutating state.

  • Methods: Calling list.size() or array.reverse().

Clojure flips this:

  • Data: Immutable collections (lists, vectors, maps) are king—no objects with hidden state.

  • Functions: Pure, standalone operations (e.g., (count coll) instead of coll.size()).

  • Sequences: The seq abstraction unifies iteration—no explicit loops, just transformations.

Mental Shift: Instead of telling objects what to do (“Hey, list, reverse yourself!”), you apply functions to data (“Take this list and give me its reverse.”).
Intuition: “Think of data as clay—functions shape it without breaking it, and I’m the sculptor passing it along a conveyor of tools.”

The 10 Minimum Functions

These are your core tools to write much of the Clojure standard library. Each gets an intuition line to make it stick.

1. seq

  • What: Turns a collection into a sequence (or nil if empty).

  • OOP Analogy: Like calling iterator() on a Java List to get an Iterator.

  • Why You Need It: It’s the gateway to sequence operations—unifies lists, vectors, maps, strings, etc.

  • Example:

    clojure

(seq [1 2 3])  ;; => (1 2 3)
(seq [])       ;; => nil
  • Intuition: “Give me a way to walk this data, step-by-step—like handing me a map and a starting point.”

2. first

  • What: Gets the first item of a sequence.

  • OOP Analogy: Like iterator.next() in Java after checking hasNext().

  • Why You Need It: Pairs with rest for recursive traversal.

  • Example:

    clojure

(first (seq [1 2 3]))  ;; => 1
(first nil)            ;; => nil
  • Intuition: “Lift the first box off the conveyor belt—what’s inside?”

3. rest

  • What: Returns the sequence of all items after the first (empty seq if none).

  • OOP Analogy: Advancing an iterator to the next position.

  • Why You Need It: Enables recursion by giving you “the rest” to process.

  • Example:

    clojure

(rest (seq [1 2 3]))  ;; => (2 3)
(rest [1])            ;; => ()
  • Intuition: “Slide the conveyor forward—show me everything after the first box.”

4. cons

  • What: Constructs a new sequence by prepending an item to an existing sequence.

  • OOP Analogy: Like list.addFirst(item) in a linked list, but immutable (returns a new list).

  • Why You Need It: Builds sequences incrementally (e.g., reversing a list).

  • Example:

    clojure

(cons 0 [1 2 3])  ;; => (0 1 2 3)
(cons 1 ())       ;; => (1)
  • Intuition: “Toss a new item onto the front of the pile, like stacking a brick on a wall.”

5. concat

  • What: Combines multiple sequences into one.

  • OOP Analogy: Like list.addAll(otherList) in Java, but immutable.

  • Why You Need It: Flattens results (e.g., in mapcat) or merges collections.

  • Example:

    clojure

(concat [1 2] [3 4])  ;; => (1 2 3 4)
(concat () [1])       ;; => (1)
  • Intuition: “Pour all these buckets of marbles into one big bucket—keep the order.”

6. lazy-seq

  • What: Creates a lazy sequence, delaying computation until needed.

  • OOP Analogy: Like a Java Stream that’s evaluated on demand (e.g., stream.filter().collect()).

  • Why You Need It: Makes infinite or large sequences practical (e.g., mapcat’s laziness).

  • Example:

    clojure

(defn count-up [n]
  (lazy-seq (cons n (count-up (inc n)))))
(take 3 (count-up 1))  ;; => (1 2 3)
  • Intuition: “Hand me a recipe for this sequence—I’ll bake it only when I’m hungry for it.”

7. map

  • What: Applies a function to each item in a sequence, returning a new sequence of results.

  • OOP Analogy: Like Java’s list.stream().map(x -> f(x)).collect().

  • Why You Need It: Core transformation tool—basis for mapcat and others.

  • Example:

    clojure

(map inc [1 2 3])  ;; => (2 3 4)
  • Intuition: “Send each item through a machine that tweaks it—collect the shiny new versions.”

8. filter

  • What: Keeps items in a sequence where a predicate returns true.

  • OOP Analogy: Like Java’s list.stream().filter(x -> p(x)).

  • Why You Need It: Selects data functionally—used in tons of library fns.

  • Example:

    clojure

(filter even? [1 2 3 4])  ;; => (2 4)
  • Intuition: “Set up a bouncer at the door—only let in items that pass the vibe check.”

9. reduce

  • What: Combines a sequence into a single value using a function.

  • OOP Analogy: Like a Java for loop with an accumulator (e.g., summing a list).

  • Why You Need It: Aggregates data—can build map, filter, etc., from it.

  • Example:

    clojure

(reduce + [1 2 3])  ;; => 6
(reduce conj [] [1 2 3])  ;; => [1 2 3]
  • Intuition: “Mix all these ingredients into one pot, stirring with a special rule.”

10. when-let

  • What: Binds a value and executes a body only if the value is non-nil.

  • OOP Analogy: Like an if (obj != null) { var x = obj; ... } block in Java.

  • Why You Need It: Cleanly handles conditional logic with sequences (e.g., mapcat’s base case).

  • Example:

    clojure

(when-let [x (seq [1 2 3])] (first x))  ;; => 1
(when-let [x (seq [])] (first x))       ;; => nil
  • Intuition: “If the box isn’t empty, open it and use what’s inside—otherwise, walk away.”

Rebuilding mapcat with Intuition

Let’s reconstruct mapcat using these tools and intuition lines:

clojure

(defn my-mapcat [f coll]
  (lazy-seq                   ;; “Hand me a recipe—I’ll bake it when needed.”
    (when-let [s (seq coll)]  ;; “If the box isn’t empty, open it and walk the data.”
      (concat                 ;; “Pour these buckets into one big bucket.”
        (f (first s))         ;; “Run the first item through the tweaking machine.”
        (my-mapcat f (rest s))))))  ;; “Slide the conveyor, repeat the process.”
(my-mapcat (fn [x] [x (* 2 x)]) [1 2 3])  ;; => (1 2 2 4 3 6)
  • Flow:

    • “Walk the data” (seq).

    • “Check the first box” (first), tweak it (f).

    • “Slide forward” (rest), recurse.

    • “Pour it all together” (concat), but “only when asked” (lazy-seq).

Intuition: “It’s like a factory line: inspect each item, transform it into parts, dump them into one pile, and only run the machines when the boss demands output.”

OOP-to-Clojure Thinking Guide with Intuition

  • Instead of Loops: Use recursion with first/rest or higher-order fns like map/reduce.

    • OOP: for (int i : list) { sum += i; }.

    • Clojure: (reduce + coll).

    • Intuition: “Don’t march through the list—let the conveyor bring it to you.”

  • Instead of Mutation: Build new data with cons, concat, reduce.

    • OOP: list.add(x).

    • Clojure: (cons x list).

    • Intuition: “Don’t change the sculpture—mold a new one from the clay.”

  • Instead of Null Checks: Use seq and when-let.

    • OOP: if (list != null && !list.isEmpty()).

    • Clojure: (when-let [s (seq list)] ...).

    • Intuition: “Ask if the box has stuff before bothering to open it.”

  • Instead of Method Chains: Pipe data through functions.

    • OOP: list.sort().filter().map().

    • Clojure: (->> coll (filter p) (map f)).

    • Intuition: “Pass the clay through a line of shaping tools—each does one job.”

New Mantra: “Data flows through functions like water through pipes—I just pick the right fittings.”


Read the original article

Comments

HackerNews