← Back to list

Category Theory Crash Course with Clojure: Part 2 — Introduction to Monoids

Building upon semigroups to unlock more powerful abstractions in your Clojure code.

Eugenii Shevchenko · 2024-09-20 11:06 · 53 claps · 3.6 min read
#monoids #category-theory #functional-programming #clojure
Open on Medium ↗
Wiki topics: TLS · Design Tools & Workflow 💻 · Programming

Category Theory Crash Course with Clojure: Part 2 — Introduction to Monoids

Building upon semigroups to unlock more powerful abstractions in your Clojure code.

Welcome back to our crash course on Category Theory using Clojure! In Part 1, we explored semigroups and how they provide a foundation for combining elements associatively. In this installment, we’ll delve into monoids, which extend semigroups by introducing an identity element. This small addition unlocks significant power in abstraction and code composition.

Recap: Semigroups

A semigroup is a set equipped with an associative binary operation. This means we can combine any two elements of the set, and the grouping of operations doesn’t affect the outcome.

(defprotocol Semigroup
  (combine [a b] "Combine two elements"))

(extend-protocol Semigroup
  String
  (combine [a b]
    (str a b)))

While semigroups are useful, they lack an identity element, which limits their flexibility in certain contexts.

What is a Monoid?

A monoid is an algebraic structure that builds upon a semigroup by adding an identity element. This element acts as a “do-nothing” value in the context of the binary operation.

Formal Definition A monoid is a triple (M, ·, e) where:

  • M is a non-empty set.
  • · is an associative binary operation (·: M × M → M).
  • e is an identity element in M such that for all a in M: e⋅a=a⋅e=a.

Key Properties

  • Associativity: (a · b) · c = a · (b · c) for all a, b, c in M.
  • Identity Element: There exists an element e in M such that e · a = a · e = a for all a in M.

Monoids in Programming

Monoids are ubiquitous in programming because they model operations where you can:

  • Combine elements associatively.
  • Have a neutral element that doesn’t change other elements when combined.

Common Examples

  • Numbers with Addition and Zero: Integers with addition and zero as the identity.
  • Strings with Concatenation and Empty String: Strings with concatenation and "" as the identity.
  • Lists with Concatenation and Empty List: Lists with concatenation and [] as the identity.

Implementing Monoids in Clojure

Let’s enhance our previous semigroup implementation by introducing the identity element.

Defining the Monoid Protocol

(defprotocol Monoid
  (identity [this] "Return the identity element")
  (combine [a b] "Combine two elements"))

Implementing Monoids for Different Types

Example 1: Numbers with Addition

(extend-protocol Monoid
  Number
  (identity [this]
    0)
  (combine [a b]
    (+ a b)))

Example 2: Strings with Concatenation

(extend-protocol Monoid
  String
  (identity [this]
    "")
  (combine [a b]
    (str a b)))

Example 3: Lists with Concatenation

(extend-protocol Monoid
  clojure.lang.IPersistentList
  (identity [this]
    '())
  (combine [a b]
    (concat a b)))

Using the Monoid Functions

;; Numbers
(combine 1 2)
;; => 3

(identity 0)
;; => 0

(combine 1 (identity 1))
;; => 1

;; Strings
(combine "Hello, " "World!")
;; => "Hello, World!"

(identity "")
;; => ""

(combine "Clojure" (identity ""))
;; => "Clojure"

;; Lists
(combine [1 2] [3 4])
;; => (1 2 3 4)

(identity [])
;; => ()

(combine [1 2] (identity []))
;; => (1 2)

Verifying Monoid Laws

Let’s write functions to test the monoid laws: associativity and identity.

Associativity Test

(defn associative? [a b c]
  (= (combine (combine a b) c)
     (combine a (combine b c))))

Identity Test

(defn identity? [a]
  (and (= (combine (identity a) a) a)
       (= (combine a (identity a)) a)))

Testing with Numbers

(associative? 1 2 3)
;; => true

(identity? 5)
;; => true

Testing with Strings

(associative? "a" "b" "c")
;; => true

(identity? "Clojure")
;; => true

Testing with Lists

(associative? [1] [2] [3])
;; => true

(identity? [1 2 3])
;; => true

Practical Use Case: Folding (Reducing) Data Structures

Monoids are particularly useful for folding (reducing) data structures because the identity element provides a natural starting point.

Example: Summing a List of Numbers

(def numbers [1 2 3 4 5])

(reduce combine (identity 0) numbers)
;; => 15

Example: Concatenating a List of Strings

(def strings ["Hello, " "world" "!"])

(reduce combine (identity "") strings)
;; => "Hello, world!"

Example: Merging Multiple Maps

First, let’s implement Monoid for maps.

(extend-protocol Monoid
  clojure.lang.IPersistentMap
  (identity [this]
    {})
  (combine [a b]
    (merge a b)))

Usage

(def maps [{:a 1} {:b 2} {:c 3}])

(reduce combine (identity {}) maps)
;; => {:a 1, :b 2, :c 3}

The Power of Monoids

By introducing an identity element, monoids enable:

  • Flexible Folding: Start with an identity value when reducing collections.
  • Composability: Chain operations without worrying about special cases.
  • Parallelism: Break down computations and combine results efficiently.

Parallel Processing Example

Imagine processing large datasets in parallel and combining the results.

(def dataset [[1 2 3] [4 5 6] [7 8 9]])

(defn process-chunk [chunk]
  (reduce combine (identity 0) chunk))

(def results (pmap process-chunk dataset))
;; => (6 15 24)

(reduce combine (identity 0) results)
;; => 45

Monoids in the Wild

Monoids are not just theoretical constructs; they are widely used in functional programming libraries.

  • Reducers and Transducers: Efficient data processing pipelines.
  • Logging: Combining log entries.
  • Configuration Merging: Combining configuration maps with defaults.

When to Use Monoids

Consider using monoids when you have:

  • A need to combine data in a way that is associative and has a neutral starting point.
  • Parallelizable operations where partial results can be combined.
  • A requirement for generic, composable code without special cases.

What’s Next?

Now that we’ve covered monoids, we’re ready to explore more complex structures like functors and applicatives, which allow us to apply functions within contexts. These abstractions will further enhance your ability to write elegant and powerful Clojure code.

Stay Tuned!

In the next installment, we’ll dive into functors and see how they enable us to map functions over computational contexts. We’ll explore practical examples and implementations in Clojure.


메타데이터
post_id
9e95ce1a5a50
slug
category-theory-crash-course-with-clojure-part-2-introduction-to-monoids-9e95ce1a5a50
url
https://medium.com/@eugenesh4work/category-theory-crash-course-with-clojure-part-2-introduction-to-monoids-9e95ce1a5a50
canonical_url
https://medium.com/@eugenesh4work/category-theory-crash-course-with-clojure-part-2-introduction-to-monoids-9e95ce1a5a50
author_url
https://medium.com/@eugenesh4work
status
ok
fetched_at
2026-07-10 13:32:34