DEV Community

Cover image for Clojure Is Awesome!!! [PART 11]
André Borba
André Borba

Posted on

Clojure Is Awesome!!! [PART 11]

Function Composition in Clojure: Building Powerful Data Transformations

1. Function Composition Pattern

Function composition is a fundamental concept in functional programming where the output of one function becomes the input of another function, creating a new function. In Clojure, this pattern is not just a feature - it's a core design philosophy.

What is it?

Function composition is the process of combining two or more functions to create a new function. The result is a pipeline where data flows through multiple transformations.

Why use it?

  • Promotes code reusability
  • Increases maintainability
  • Reduces complexity
  • Makes code more testable
  • Enhances readability

When to use:

  • Data transformation pipelines
  • Request/Response middleware
  • Event processing chains
  • Input validation sequences
  • Complex calculations

When not to use:

  • Simple, single-step operations
  • When imperative flow control is needed
  • When state management is the primary concern
  • When direct recursion is more appropriate

2. Threading Macros: The Clojure Way

Threading macros are Clojure's elegant solution for composing functions in a readable, maintainable way.

The -> (Thread-first) Macro

  • Inserts each expression as the first argument in the next form
  • Perfect for object-oriented style transformations
  • Ideal when working with maps and associative operations
(-> person
    (assoc :name "John")
    (update :age inc)
    (dissoc :temporary-field))
Enter fullscreen mode Exit fullscreen mode

The ->> (Thread-last) Macro

  • Inserts each expression as the last argument in the next form
  • Perfect for collection transformations
  • Ideal for operations like map, filter, reduce
(->> numbers
     (filter even?)
     (map #(* % 2))
     (reduce +))
Enter fullscreen mode Exit fullscreen mode

The some-> and some->> Macros

  • Similar to -> and ->>, but short-circuit on nil values
  • Perfect for handling potentially nil values
  • Great for optional transformations
(some-> user
        :address
        :city
        str/upper-case)
Enter fullscreen mode Exit fullscreen mode

3. Partial and Comp: Function Transformers

Partial Function Application

  • Creates a new function with some arguments pre-supplied
  • Reduces function arity
  • Perfect for specialization
(def add-five (partial + 5))
(def positive? (partial < 0))
Enter fullscreen mode Exit fullscreen mode

Function Composition with comp

  • Combines multiple functions into a single function
  • Applies functions from right to left
  • Perfect for creating transformation pipelines
(def format-price
  (comp str/upper-case
        #(str "$ " %)
        #(format "%.2f" %)))
Enter fullscreen mode Exit fullscreen mode

4. Function Pipelines

(ns function-composition
  (:require [clojure.string :as str]
            [clojure.pprint :as pp]))

;; === Protocol Definition ===
(defprotocol OrderProcessor
  (process-order [this order]))

;; === Data Processing Pipeline Example ===
(defn clean-string
  "Removes special characters and converts to lowercase"
  [s]
  (-> s
      str/lower-case
      (str/replace #"[^a-z0-9\s]" "")
      str/trim))

(defn word-frequencies
  "Counts word frequencies in a text"
  [text]
  (->> (str/split text #"\s+")
       (map clean-string)
       (filter (complement str/blank?))
       (frequencies)
       (sort-by val >)
       (into {})))

;; === Financial Calculations Pipeline ===
(def tax-rate 0.15)
(def discount-rate 0.10)

(def calculate-tax (partial * (+ 1 tax-rate)))
(def apply-discount (partial * (- 1 discount-rate)))

(def process-price
  (comp (partial format "%.2f")
        apply-discount
        calculate-tax))

;; === Request Processing Pipeline ===
(defn validate-user [request]
  (some-> request
          (assoc :validated true)
          (update :timestamp #(or % (System/currentTimeMillis)))))

(defn enrich-data [request]
  (some-> request
          (assoc :enriched true)
          (update :data merge {:extra "info"})))

(defn format-response [request]
  (some-> request
          (update :data #(select-keys % [:important :fields]))
          (dissoc :internal-only)))

(def process-request
  (comp format-response
        enrich-data
        validate-user))

;; === Order Processing Functions ===
(defn calculate-item-total
  "Calculates total for a single item"
  [{:keys [quantity price]}]
  (* quantity price))

(defn calculate-items-total
  "Calculates total for all items"
  [items]
  (->> items
       (map calculate-item-total)
       (reduce +)))

(defn apply-promotions
  "Applies a sequence of promotional discounts"
  [total promotions]
  (reduce (fn [price discount]
            (- price (* price discount)))
          total
          promotions))

(defn format-money
  "Formats a number as currency"
  [amount]
  (format "$ %.2f" amount))

;; === Order Processor Implementation ===
(defrecord StandardOrderProcessor []
  OrderProcessor
  (process-order [_ {:keys [items promotions]}]
    (let [subtotal (calculate-items-total items)
          total-with-promos (apply-promotions subtotal promotions)]
      {:subtotal subtotal
       :total total-with-promos
       :formatted-total (format-money total-with-promos)
       :items-count (count items)
       :processed-at (java.time.Instant/now)})))

;; === Example Usage and Demonstrations ===
(defn run-examples []
  (println "\n=== Word Frequency Analysis ===")
  (let [text "The quick brown fox jumps over the lazy dog. The FOX is quick!"]
    (println "Original text:" text)
    (println "Word frequencies:")
    (pp/pprint (word-frequencies text)))

  (println "\n=== Financial Calculations ===")
  (let [original-price 100.00]
    (println "Original price:" original-price)
    (println "Processed price:" (process-price original-price)))

  (println "\n=== Request Processing ===")
  (let [request {:data {:important "value"
                       :internal-only "secret"
                       :fields "data"}}]
    (println "Processed request:")
    (pp/pprint (process-request request)))

  (println "\n=== Order Processing ===")
  (let [order {:items [{:quantity 2 :price 10.0}
                       {:quantity 1 :price 25.0}]
               :promotions [0.1 0.05]}
        processor (->StandardOrderProcessor)]
    (println "Processed order:")
    (pp/pprint (process-order processor order))))

(run-examples)
Enter fullscreen mode Exit fullscreen mode

Conclusion

This exploration of Function Composition in Clojure demonstrates the language's elegant approach to building complex data transformations from simple, reusable functions. The examples we've covered show how Clojure's functional primitives and composition tools work together to create clean, maintainable, and powerful code.

  1. Threading Macros (->, ->>, some->, some->>) make data transformations readable and maintainable, as shown in our word frequency analysis example.
  2. Partial and Comp enable powerful function transformations, demonstrated in our financial calculations pipeline where we composed tax and discount operations.
  3. Function Pipelines allow us to break complex operations into small, focused functions that can be composed together, as seen in our request processing system.
  4. The Order Processing System shows how these concepts come together in a real-world scenario, combining protocols, records, and function composition to create a flexible and extensible solution.

Function composition in Clojure isn't just a programming technique—it's a way of thinking about problem-solving. By breaking down complex operations into smaller, composable functions, we create code that is:

  • Easier to test
  • Simpler to maintain
  • More reusable
  • More readable
  • More reliable

These examples demonstrate why Clojure's approach to function composition is so powerful for building robust, scalable systems.

Remember: "Good function composition is like building with LEGO blocks—each piece should be simple and focused, but when combined, they can create something sophisticated and powerful."

Top comments (0)