Complex Numbers in Clojure: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Clojure Complex Numbers: The Complete Guide from Zero to Hero

A complex number in Clojure is typically represented as a record or map containing a real and an imaginary part. This guide explains how to define this data structure and implement core arithmetic operations like addition, multiplication, division, conjugate, and absolute value using pure, functional Clojure code.


The Wall You Hit With "Normal" Numbers

Remember in school when you first learned about numbers? You started with counting numbers (1, 2, 3), then whole numbers (adding 0), then integers (adding negatives), and finally rational and real numbers. Each step expanded your ability to solve more problems. But there was always a hard wall you couldn't pass: what is the square root of -1?

Equations like x² + 1 = 0 were deemed "unsolvable" within the realm of real numbers. This is a frustrating limitation, especially when these problems appear in advanced fields like physics, signal processing, and electrical engineering. The solution isn't to give up; it's to expand our number system one more time.

This is where complex numbers come in. They provide an elegant way to solve these "impossible" equations and model real-world phenomena. In this guide, you'll learn not just the theory behind complex numbers, but how to build a robust implementation from scratch using the power and elegance of Clojure. We'll turn abstract math into concrete, functional code, transforming you from a novice to a confident practitioner.


What Exactly Are Complex Numbers?

A complex number is a number that can be expressed in the form a + bi, where a and b are real numbers, and i is the "imaginary unit." This special unit is the cornerstone of complex numbers, defined by the property i² = -1.

  • Real Part (a): This is the familiar real number component. If b is zero, the complex number is just a real number.
  • Imaginary Part (b): This is the real number that multiplies the imaginary unit i. If a is zero, the number is a "purely imaginary" number.

Think of it as a two-dimensional number. While real numbers can be plotted on a single line (the number line), complex numbers require a two-dimensional plane, often called the "complex plane." The horizontal axis represents the real part (a), and the vertical axis represents the imaginary part (b).

The Anatomy of a + bi

  • If b = 0, we have a + 0i = a. This shows that all real numbers are also complex numbers.
  • If a = 0, we have 0 + bi = bi. This is called a purely imaginary number.
  • The imaginary unit i itself is the complex number 0 + 1i.

This structure allows us to perform standard arithmetic operations, but with a few new rules to handle the imaginary unit i.


Why Should a Programmer Care About Complex Numbers?

It's easy to dismiss complex numbers as a purely mathematical curiosity, but they have profound and practical applications in technology and science. Understanding them unlocks capabilities in several important domains:

  • Electrical Engineering: In AC circuits, complex numbers (specifically "phasors") are used to describe the phase and amplitude of sinusoidal voltages and currents. This simplifies the analysis of circuits containing resistors, capacitors, and inductors.
  • Signal Processing: The Fourier Transform, a fundamental tool for analyzing signals, decomposes a signal into its constituent frequencies. This process inherently uses complex numbers to represent both the magnitude and phase of each frequency component. It's crucial for audio processing, image compression (like JPEG), and telecommunications.
  • Computer Graphics & Game Development: Complex numbers are related to quaternions, which are used extensively in 3D graphics to represent rotations. They avoid problems like "gimbal lock" that can occur with other rotation methods. Furthermore, beautiful fractal images like the Mandelbrot set are generated by iterating calculations on complex numbers.
  • Quantum Mechanics: The state of a quantum system is described by a wave function, which is a complex-valued function. Complex numbers are not just a convenience here; they are fundamental to the mathematical framework of quantum physics.
  • Control Theory: Engineers use the complex plane (specifically the s-plane) to analyze the stability and behavior of dynamic systems, like the autopilot of an aircraft or the cruise control in a car.

By learning to model them in Clojure, you're not just solving a puzzle from the kodikra learning path; you're building a tool that has direct relevance to high-tech fields.


How to Represent and Implement Complex Numbers in Clojure

Now we get to the core of the problem: translating the mathematical concept of a complex number into Clojure data structures and functions. Our goal is to create a system that can handle all the standard operations cleanly and efficiently.

Choosing the Right Data Structure: defrecord vs. map

In Clojure, we have a few options for representing a compound data type like a complex number. The two most common choices are a simple map or a record defined with defrecord.

A map like {:real 3 :imag -5} is flexible and easy to create. However, a defrecord offers several key advantages for this use case:

  • Performance: Records are generally faster for field access than maps because they are compiled down to Java classes.
  • Structure: A defrecord enforces a specific set of keys, preventing typos and ensuring that every "complex number" in our system has a consistent shape.
  • Semantics: It creates a distinct type. We can check if something is a complex number with (instance? ComplexNumber z), which is cleaner than checking for the presence of specific map keys.
  • Extensibility: Records are designed to work with Clojure's protocols, allowing us to extend their behavior in powerful ways later on.

For these reasons, defrecord is the superior choice for this module. We'll define our complex number type like this:


;; Defines a new type, Complex, with two fields: :real and :imag.
;; This creates a constructor function `->Complex` and allows
;; field access like (:real z) or (.real z).
(defrecord Complex [real imag])

This single line gives us a new type Complex, a constructor function ->Complex (e.g., (->Complex 2 3)), and the ability to treat it like a map for reading values.

The Complete Clojure Solution

Here is the full implementation for the complex numbers module from the kodikra.com curriculum. We'll define a namespace and then implement each required function one by one.


(ns complex-numbers
  (:require [clojure.math :as math]))

;; Define the Complex Number type using defrecord for structure and performance.
(defrecord Complex [real imag])

;; Helper function to create a new Complex number instance.
;; This provides a more semantic constructor than the default ->Complex.
(defn complex [a b]
  (->Complex a b))

;; --- Accessor Functions ---

;; Extracts the real part of a complex number.
(defn real [z]
  (:real z))

;; Extracts the imaginary part of a complex number.
(defn imag [z]
  (:imag z))

;; --- Core Arithmetic Operations ---

;; Adds two complex numbers: (a + bi) + (c + di) = (a + c) + (b + d)i
(defn add [z1 z2]
  (->Complex
   (+ (:real z1) (:real z2))
   (+ (:imag z1) (:imag z2))))

;; Subtracts two complex numbers: (a + bi) - (c + di) = (a - c) + (b - d)i
(defn sub [z1 z2]
  (->Complex
   (- (:real z1) (:real z2))
   (- (:imag z1) (:imag z2))))

;; Multiplies two complex numbers: (a + bi) * (c + di) = (ac - bd) + (ad + bc)i
(defn mul [z1 z2]
  (let [a (:real z1)
        b (:imag z1)
        c (:real z2)
        d (:imag z2)]
    (->Complex
     (- (* a c) (* b d))  ; New real part
     (+ (* a d) (* b c))))) ; New imaginary part

;; Divides two complex numbers: (a + bi) / (c + di)
;; Formula: [(ac + bd) / (c² + d²)] + [(bc - ad) / (c² + d²)]i
(defn div [z1 z2]
  (let [a (:real z1)
        b (:imag z1)
        c (:real z2)
        d (:imag z2)
        denominator (+ (* c c) (* d d))]
    (->Complex
     (/ (+ (* a c) (* b d)) denominator) ; New real part
     (/ (- (* b c) (* a d)) denominator)))) ; New imaginary part

;; --- Other Operations ---

;; Calculates the absolute value (modulus) of a complex number.
;; |z| = sqrt(a² + b²)
(defn abs [z]
  (math/sqrt
   (+ (math/pow (:real z) 2)
      (math/pow (:imag z) 2))))

;; Calculates the conjugate of a complex number.
;; conjugate of (a + bi) is (a - bi)
(defn conjugate [z]
  (->Complex (:real z) (- (:imag z))))

```

Detailed Code Walkthrough

Let's break down the logic behind the most important functions to understand how the mathematical formulas are translated into functional Clojure code.

1. Multiplication Logic (mul function)

Multiplying two complex numbers, (a + bi) and (c + di), works just like multiplying two binomials in algebra, remembering that i² = -1.

(a + bi) * (c + di) = a(c + di) + bi(c + di)
= ac + adi + bci + bdi²
= ac + adi + bci - bd (since i² = -1)
= (ac - bd) + (ad + bc)i (grouping real and imaginary terms)

The final result gives us the formula for the new real part (ac - bd) and the new imaginary part (ad + bc). Our Clojure code implements this directly.


(defn mul [z1 z2]
  (let [a (:real z1)
        b (:imag z1)
        c (:real z2)
        d (:imag z2)]
    (->Complex
     (- (* a c) (* b d))  ; New real part: (ac - bd)
     (+ (* a d) (* b c))))) ; New imaginary part: (ad + bc)

The let block destructures the real and imaginary parts into local variables a, b, c, and d for clarity. Then, it constructs a new Complex record using the derived formulas.

Here is a visual representation of the multiplication flow:

    ● Start: mul(z1, z2)
    │
    ├─ z1: (a + bi)
    └─ z2: (c + di)
    │
    ▼
  ┌───────────────────┐
  │ Destructure Parts │
  │ a, b, c, d        │
  └─────────┬─────────┘
            │
   ╭────────┴────────╮
   │                 │
   ▼                 ▼
┌───────────┐     ┌───────────┐
│ Calc Real │     │ Calc Imag │
│ (ac - bd) │     │ (ad + bc) │
└───────────┘     └───────────┘
   │                 │
   ╰────────┬────────╯
            │
            ▼
  ┌───────────────────┐
  │ Construct Result  │
  │ ->Complex(real, imag) │
  └───────────────────┘
            │
            ▼
    ● End: New Complex Number

2. Division Logic (div function)

Division is the most complex operation. To divide (a + bi) by (c + di), we can't simply divide the parts. The trick is to eliminate the imaginary unit i from the denominator. We achieve this by multiplying both the numerator and the denominator by the conjugate of the denominator.

The conjugate of (c + di) is (c - di). Multiplying a complex number by its conjugate always results in a real number: (c + di)(c - di) = c² - (di)² = c² - d²i² = c² + d².

So, the division becomes:

(a + bi) (a + bi) * (c - di) (ac - adi + bci - bdi²)
-------- = ------------------- = -----------------------
(c + di) (c + di) * (c - di) c² + d²

Simplifying the numerator:

= (ac + bd) + (bc - ad)i

Splitting this into the real and imaginary parts over the real denominator:

= [ (ac + bd) / (c² + d²) ] + [ (bc - ad) / (c² + d²) ]i

This gives us the final formulas for the new real and imaginary parts, which are implemented in our div function.


(defn div [z1 z2]
  (let [a (:real z1)
        b (:imag z1)
        c (:real z2)
        d (:imag z2)
        denominator (+ (* c c) (* d d))] ; This is c² + d²
    (->Complex
     (/ (+ (* a c) (* b d)) denominator) ; New real part
     (/ (- (* b c) (* a d)) denominator)))) ; New imaginary part

The logic flow for division is visualized below:

    ● Start: div(z1, z2)
    │
    ├─ Numerator:   z1 = (a + bi)
    └─ Denominator: z2 = (c + di)
    │
    ▼
  ┌──────────────────────────┐
  │ Find Conjugate of z2     │
  │ z2_conj = (c - di)       │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Multiply Numerator by z2_conj │
  │ num_new = (ac+bd) + (bc-ad)i │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Multiply Denominator by z2_conj │
  │ den_new = c² + d² (a real number) │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Divide Parts Separately  │
  ├─ real = (ac+bd) / (c²+d²) │
  └─ imag = (bc-ad) / (c²+d²) │
  └────────────┬─────────────┘
               │
               ▼
    ● End: New Complex Number

Running the Code in a REPL

You can test this code interactively using a Clojure REPL (Read-Eval-Print Loop). Save the code as src/complex_numbers.clj. Then, start a REPL from your terminal.


$ clj
Clojure 1.11.1
user=>

Now, load the namespace and start using the functions:


user=> (require '[complex-numbers :as cn])
nil
user=> (def z1 (cn/complex 1 2))
#'user/z1
user=> (def z2 (cn/complex 3 4))
#'user/z2
user=> (cn/add z1 z2)
#complex_numbers.Complex{:real 4, :imag 6}
user=> (cn/mul z1 z2)
#complex_numbers.Complex{:real -5, :imag 10}
user=> (cn/abs (cn/complex 3 -4))
5.0

Alternative Approaches and Future-Proofing

While defrecord is an excellent choice, it's worth knowing about other ways to model this data and how to make the system more robust for future use.

Alternative: Using Plain Maps

You could implement all the functions using simple maps, like {:real 1, :imag 2}. The functions would look very similar, just without the ->Complex constructor.

Pros: * More flexible; you can add extra keys to a map easily. * No need to define a type upfront.

Cons: * Slower field access. * No type safety; (add {:real 1} {:x 2}) would cause a runtime error instead of a more logical type mismatch. * Less semantic; it's just a map, not explicitly a "Complex Number".

Pros & Cons: defrecord vs. map

Feature defrecord map
Performance Higher (JVM class-based) Lower (hash map lookups)
Structure Rigid, enforced fields Flexible, arbitrary keys
Type Semantics Strong (creates a new type) Weak (it's just a map)
Extensibility Excellent with Protocols Limited; requires manual checks
Ease of Use Slightly more setup (one line) Immediately available

Future-Proofing with Protocols

For a more advanced, production-grade system, you could define the behavior using Clojure's Protocols. A protocol defines a set of functions that a data type can implement.


(defprotocol IComplexOps
  (add [this other])
  (mul [this other]))

;; Then, you can extend your Complex type to implement this protocol.
;; This decouples the function definitions from the data type.
(extend-type Complex
  IComplexOps
  (add [z1 z2]
    (->Complex (+ (:real z1) (:real z2))
               (+ (:imag z1) (:imag z2))))
  (mul [z1 z2]
    ;; ... multiplication logic ...
    ))

This approach is common in larger libraries as it allows different types (e.g., a Complex number and a regular Number) to participate in the same operations, a concept known as polymorphism.

As functional programming patterns become more prevalent in data science and concurrent systems, building such well-defined, extensible data types is a critical skill. Libraries for scientific computing in Clojure, like core.matrix, use these principles to handle a wide variety of numerical types, including complex numbers.


Frequently Asked Questions (FAQ)

Why is the imaginary unit `i` so important?

The imaginary unit i is the fundamental building block that extends the real number line into the two-dimensional complex plane. By defining i² = -1, it provides a solution to polynomial equations that were previously unsolvable, which in turn allows for the modeling of many physical phenomena involving periodic or rotational motion.

Why build this from scratch instead of using a library?

While libraries like Apache Commons Math (for Java interop) or native Clojure libraries exist, building it yourself is a core learning exercise. This kodikra module teaches you about data modeling (defrecord), implementing mathematical formulas in a functional style, and understanding the trade-offs of different data structures in Clojure. It solidifies your understanding of both the mathematical concepts and the programming language.

Could I represent a complex number with a vector like `[a b]`?

Yes, you could use a two-element vector, e.g., [3 4] to represent 3 + 4i. You would access the parts using (nth z 0) for real and (nth z 1) for imaginary. This is very performant but lacks semantic clarity. It's not obvious that [3 4] is a complex number and not just a 2D point, which can lead to confusing code.

How does Clojure's immutability help in this implementation?

Immutability is a huge benefit here. When you call (add z1 z2), you are not changing z1 or z2. Instead, a completely new Complex number is returned. This prevents a whole class of bugs called "side effects," where a function unexpectedly modifies its inputs. It makes the code easier to reason about, test, and use in concurrent applications.

What is the most common mistake when implementing complex number division?

The most common mistake is forgetting to use the conjugate. A naive attempt might be to divide the real and imaginary parts separately, which is mathematically incorrect. The key is to multiply both the numerator and denominator by the conjugate of the denominator to make the new denominator a real number.

Is Clojure a good language for mathematical computing?

Clojure is an excellent language for mathematical and scientific computing. Its functional nature, immutable data structures, and powerful REPL make it ideal for interactive exploration and building robust, correct algorithms. Combined with its seamless Java interoperability, it can leverage a vast ecosystem of high-performance math and science libraries.

Where can I learn more about the applications of complex numbers?

A great starting point is learning about the Fourier Transform, as it's a cornerstone of modern signal processing. For a more visual application, look into tutorials on generating the Mandelbrot set fractal. Both of these topics beautifully illustrate the power of complex numbers in practice.


Conclusion: From Abstract Math to Concrete Code

We've successfully journeyed from the abstract mathematical concept of a complex number to a concrete, functional, and idiomatic Clojure implementation. By choosing defrecord, we created a data structure that is performant, structured, and semantically clear. We translated the core arithmetic formulas for addition, subtraction, multiplication, and division into pure functions that are easy to test and reason about.

This exercise from the kodikra.com curriculum is more than just a coding challenge; it's a lesson in data modeling and the power of functional programming to solve real-world problems elegantly. The skills you've honed here are directly applicable to building more complex systems in any domain.

Disclaimer: All code in this article is written and tested against Clojure 1.11.x, the latest stable version at the time of writing. The fundamental concepts and functions are stable and should remain compatible with future versions of Clojure.

Ready to tackle the next challenge? Continue your journey on the Clojure Learning Path to build on these skills. Or, if you want to deepen your general knowledge, you can explore more Clojure topics on our main page.


Published by Kodikra — Your trusted Clojure learning resource.