A while ago, I started working on a little side
project: a grapher for functions of
complex numbers. The first iteration of this project was written in JavaScript.
The implementation was, in hindsight, pretty bad: inelegant, ugly, and most
importantly, difficult to maintain. This was partially because of a lack of
programming experience on my part, and partially because JavaScript is a
horrible, unintuitive abomination of a language. A few years later, I learned
about Clojure, and my mind was blown. That’s when I decided to completely
scrap my JavaScript project, and rewrite the entire thing in ClojureScript.
Needless to say, I am much happier with the code quality of the rewritten project. Sure, part of this can be attributed to the fact that I became a better coder after a few more years of experience. But I think a huge factor was the Clojure language itself. The functional, data-oriented style makes you think about problems in a completely different (and of course, beneficial) way. And then there’s the many built-in features of Clojure, that allowed me to solve problems in ways far more elegant than was ever possible in JavaScript. Here, I’m going to describe how I used one feature which I think is particularly neat: protocols.
Before I talk about what Clojure protocols are and how I used them, I’ll have to back up a bit and talk about the problem that they solved. And to do that, I’ll have to take you back to math class, and talk about complex numbers. If you already know what they are, you should skip ahead to the next section.
To avoid delving too deep into the mathematics, I’ll keep this brief, albeit slightly inaccurate. There’s a number called $i$, which is equal to $\sqrt{-1}$. Yes, you read that correctly: that’s a square root of a negative number. This might not make a whole lot of sense, but just roll with it. We call multiples of $i$ imaginary numbers. $3i$, $\pi i$, and $-4.6i$ are all examples of imaginary numbers. On the other hand, the numbers you are familiar with from high school, like $3$, $\pi$, and $-4.6$, are called real numbers. Finally, if we take an imaginary number and add it to a real number, we get a complex number. For example, $3 + \pi i$ is a complex number. In general, we express a complex number as $a+bi$, which is called Cartesian form. Here, $a$ is called the real part of the complex number, and $b$ is called the imaginary part.
It’s important to note that every real number is also a complex number! Every real number $x$ is actually equal to $x + 0i$.
Much like real numbers, we can perform arithmetic on complex numbers. Some laws of complex arithmetic are actually quite simple. For example, given two complex numbers $a_1+b_1i$ and $a_2+b_2i$, their sum is simply $(a_1+a_2)+(b_1+b_2)i$.
The rule for multiplication, however, is not so simple. To write it down in a sane way, we need an alternative way of expressing complex numbers, called polar form:
\[\begin{aligned} a + bi = re^{i\varphi} \end{aligned}\]$r$ is called the magnitude, and $\varphi$ is called the argument. Don’t worry about how this works exactly; just know that we can always convert between Cartesian form and polar form with these formulas:
\[\begin{aligned} & r = \sqrt{a^2+b^2} \\ & \varphi = \text{atan2}(b, a) \end{aligned}\]Given two complex numbers in polar form, $r_1e^{i\varphi_1}$ and $r_2e^{i\varphi_2}$, their product is $r_1r_2e^{i(\varphi_1+\varphi_2)}$.
It is possible to perform any other arithmetic operation on complex numbers, including division and exponentiation. You can even calculate the sine and cosine of a complex number, or the logarithm of a complex number. However, these operations are a lot more complex (pun intended), so I won’t go into detail about them here.
Back to the realm of programming. For my project, I needed to implement a complex arithmetic library. First of all, I needed a way to store complex numbers; in other words, I needed to create a complex number type. In my JavaScript implementation, I used an object prototype. Here’s a snippet of it:
function Complex(real, imaginary) {
this.re = real;
this.im = imaginary;
// ...
}
The prototype also had methods for computing the argument and magnitude of a complex number. As it turns out, every other operation can be computed using only the real part, imaginary part, argument, and magnitude. So I decided to not have any more methods in the prototype, and wrote a function for every arithmetic operation I needed. Here’s the function for addition:
function Add(c1, c2) {
return new Complex(c1.re + c2.re, c1.im + c2.im);
}
The function takes in two instances of Complex
, and spits out an instance of
Complex
. However, what if I want to pass in a JavaScript number
as one of
the parameters instead? After all, every real number is a complex number, so I
should be able to add together a number
and a Complex
. One way to solve
this would be to check the parameter types at runtime, and dynamically execute
different logic based on those types. However, this would force me to tediously
write several implementations of every single operation. Instead, I opted for
the (still tedious) solution of wrapping every single number I needed in
boilerplate:
const compE = new Complex(Math.E, 0);
const comp1 = new Complex(1, 0);
const comp2 = new Complex(2, 0);
const compPi = new Complex(Math.PI, 0);
// ...
This is garbage. What we really want is to extend the built-in number
type:
if it had methods which returned a number
’s real part, imaginary part,
argument, and magnitude, we would be able to pass in a number
to the complex
arithmetic functions without any additional effort.
Extending built-in types is possible in JavaScript. This technique is known by the less-than-flattering name of “monkey patching”, and is generally frowned upon. It is not idiomatic, and can lead to tangible issues (for example, two components monkey patching the same function).
This situation is an example of the expression problem: how can we design our software in such a way that we can easily extend an interface, as well as easily add new types which implement that interface? The JavaScript language is not at all helpful in solving this problem. Although there is a way to solve the expression problem in JavaScript, it is neither idiomatic nor elegant.
Enter Clojure protocols: an idiomatic, elegant way of solving this problem. Let’s see how I used them in my project.
A protocol is essentially an interface. It consists of a list of method signatures: the method names, and the parameters that they take in. Types can then provide concrete implementations of those methods. In my project, I created a protocol for complex arithmetic:
(defprotocol ComplexArithmetic "Perform the basic arithmetic of the complex numbers."
(re [this] "The real part of the complex number.")
(im [this] "The imaginary part of the complex number.")
(arg [this] "The argument of the complex number, in radians.")
(mag [this] "The magnitude of the complex number."))
This protocol defines an interface with four methods: re
, im
, arg
, and
mag
.
The next step was to create a concrete type, which will eventually implement
the methods in this interface. One way of doing this in Clojure is using
records, which are sort of like structs: they are a collection of fields,
each of which has a value. I created a ComplexNumber
record, with two fields,
real
and imaginary
, which store the real part and imaginary part of the
complex number, respectively.
(defrecord ComplexNumber [real imaginary])
(defn complex-from-cartesian [real imaginary]
"Create a complex number by specifying cartesian coordinates."
(->ComplexNumber real imaginary))
(defn complex-from-polar [argument magnitude]
"Create a complex number by specifying polar coordinates."
(->ComplexNumber (* magnitude (Math/cos argument))
(* magnitude (Math/sin argument))))
Below the record definition are two “constructor” functions, which are used to
create instances of ComplexNumber
. Their purpose is to completely abstract
away the internal details of ComplexNumber
.
Now, we need to make the ComplexNumber
record implement the interface defined
in ComplexArithmetic
. In Clojure, we can do this using extend-type
:
(extend-type ComplexNumber
ComplexArithmetic
(re [this]
(:real this))
(im [this]
(:imaginary this))
(arg [this]
(Math/atan2 (im this) (re this)))
(mag [this]
(Math/sqrt (+ (Math/pow (re this) 2)
(Math/pow (im this) 2)))))
Here, we defined an implementation for each of the methods laid out in the
ComplexArithmetic
protocol. re
and im
simply pass along the values of
the real
and imaginary
fields. arg
and mag
are computed using the
formulas which I described above.
We can now get to implementing the operations of complex arithmetic! Here’s how addition and multiplication are implemented:
(defn add [x y] "Adds the given complex numbers together."
(complex-from-cartesian (+ (re x) (re y))
(+ (im x) (im y))))
(defn mul [x y] "Multiplies the given complex numbers together."
(complex-from-polar (+ (arg x) (arg y))
(* (mag x) (mag y))))
Note that x
and y
don’t have to be instances of ComplexNumber
: they can
be any type, as long as that type implements the interface defined in
ComplexArithmetic
.
And now for the best part. We want to extend the built-in type number
, so
that it also implements ComplexArithmetic
. We can do this with extend-type
as well:
(extend-type number
ComplexArithmetic
(re [this]
this)
(im [this]
0)
(arg [this]
0)
(mag [this]
this))
And that’s it! We can now add a number
and a ComplexNumber
; the following
line of code, which adds $5$ and $1+2i$, is completely valid:
(add 5 (complex-from-cartesian 1 2))