Records and Protocols

In this chapter, you will write études that use defprotocol and defrecord to implement addition, subtraction, multiplication, and division of rational and complex numbers.

As an example, we will build a record that keeps track of a duration in terms of minutes and seconds, and implement a protocol that can add two durations and can convert a duration to a string. It is in a project named proto.

(defrecord Duration [min sec])

Once you have this record defined, you can use it as follows:

proto.core=> 
proto.core=> (def d (Duration. 2 29)) ;; Create a new duration of 2 minutes and 29 seconds
#proto.core.Duration{:min 2, :sec 29}
proto.core=> (:min d) ;; extract values
2
proto.core=> (:sec d)
29

Since a duration is a special kind of number, we will implement a protocol for handling special numbers. It has two methods: plus (to add two special numbers) and canonical (to convert the special number to “canonical form.” For example, the canonical form of 2 minutes and 73 seconds is 3 minutes and 13 seconds.

(defprotocol SpecialNumber
    (plus [this other])
    (canonical [this]))

The plus method takes two parameters: this record and an other duration. When you define protocols, the first parameter of every method is the object you are interested in manipulating.

Now you can implement these methods by adding to defrecord. Here is the code for canonical

(defrecord Duration [min sec]
    SpecialNumber

    (plus [this other]
        "Just add minutes and seconds part,
        and let canonical do the rest."
        (let [m (+ (:min this) (:min other))
              s (+ (:sec this) (:sec other))]
            (canonical (Duration. m s))))

    (canonical [this]
        (let [s (mod (:sec this) 60)
              m (+ (:min this) (quot (:sec this) 60))]
            (Duration. m s))))

And it works:

proto.core=> (canonical (Duration. 2 29))
#proto.core.Duration{:min 2, :sec 29}
proto.core=> (canonical (Duration. 2 135))
#proto.core.Duration{:min 4, :sec 15}
proto.core=> (plus (Duration. 2 29) (Duration. 3 40))
#proto.core.Duration{:min 6, :sec 9}

That’s all very nice, but what if you want to display the duration in a form that looks nice, like 2:09? You can do this by implementing the toString method of the Object protocol. Add this code to the defrecord:

    Object
    (toString [this]
        (let [s (:sec this)]
            (str (:min this) ":" (if (< s 10) "0" "") s)))

And voilà; str will now convert your durations properly:

proto.core=> (str (Duration. 4 45))
"4:45"

Étude 7-1: Rational Numbers

Clojure has rational numbers; if you enter (/ 6 8) in the REPL, you get back 3/4. ClojureScript doesn’t do that, so you will implement rational numbers by adding the minus, mul, and div methods to the SpecialNumbers protocol. You will then define a record named Rational for holding a rational number using its numerator and denominator. Implement all the methods of the protocol for rational numbers (including canonical and toString).

The canonical form of a rational number is the fraction reduced to lowest terms, with the denominator always positive; thus:

proto.core=> (canonical (Rational. 6 8))
#proto.core.Rational{:num 3, :denom 4}
proto.core=> (canonical (Rational. 6 -9))
#proto.core.Rational{:num -2, :denom 3}

To reduce a fraction, you divide its numerator and denominator by the greatest common divisor (GCD) of the two numbers. The GCD is defined only for positive numbers. Here is Dijkstra’s algorithm for GCD of numbers m and n:

The cool thing about this algorithm for finding the greatest common divisor is that it doesn’t do any division at all! Notice that it is recursively defined, so this is a wonderful place for you to learn to use recur. (Hint: cond is also quite useful here.)

When converting to canonical form, if you have a zero in the numerator, just keep the rational number exactly as it is.

See a suggested solution: Solution 7-1.

Étude 7-2: Complex Numbers

Extend this project further by adding a record and protocol for complex numbers. A complex number has the form a + bi, where a is the real part and b is the imaginary part. The letter i stands for the square root of negative 1.:

Here are formulas for doing arithmetic on complex numbers.

( a + bi ) + ( c + di ) = ( a + c ) + ( b + d ) i ( a + bi ) ( c + di ) = ( a c ) + ( b d ) i ( a + bi ) ( c + di ) = ( ac bd ) + ( bc + ad ) i a + bi c + di = ( ac + bd c 2 + d 2 ) + ( bc ad c 2 + d 2 ) i

The canonical form of a complex number is just itself. Here is what conversion of complex numbers to strings should look like:

proto.core=> (str (Complex. 3 7))
"3+7i"
proto.core=> (str (Complex. 3 -7))
"3-7i"
proto.core=> (str (Complex. 3 0))
"3"
proto.core=> (str (Complex. 0 3))
"3i"
proto.core=> (str (Complex. 0 -3))
"-3i"
proto.core=> (str (Complex. 0 7))
"7i"
proto.core=> (str (Complex. 0 -7))
"-7i"

See a suggested solution: Solution 7-2.

Étude 7-3: Writing Tests

Through the book so far, I have been very lax in writing unit tests for my code. At least for this chapter, that changes.

Many projects put their tests in a separate test folder, so you should create one now, and, inside of it, make a file named test_cases.cljs. Then give it these contents (they presume that your project is named proto).

(ns ^:figwheel-always test.test-cases
  (:require-macros [cljs.test :refer [deftest is are]])
  (:require [cljs.test :as t]
            [proto.core :as p]))

Notice that the namespace is test-cases; the file name is translated to test_cases.

The ^:figwheel-always is metadata that tells Figwheel to reload the code on every change to the file.

The :require-macros is something new; macros are like functions, except that they generate ClojureScript code. The three macros that you will use are deftest, is, and are. First, let’s define a test that will check that the canonical form of 3 minutes and 84 seconds is 4 minutes and 24 seconds.

(deftest duration1
  (is (= (p/canonical (p/Duration. 3 84)) (p/Duration. 4 24))))

The deftest macro creates the test, and the is macro makes a testable assertion; the body of is should yield a boolean value. You can run tests from the REPL.

cljs.user=> (in-ns 'proto.core)
nil
proto.core=> (require '[cljs.test :as t])
nil
proto.core=> (t/run-tests 'test.test-cases)

Testing test.test-cases

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
nil

If you want to test several additions, you could write several different deftests, but if they all follow the same model, you can use are, which is followed by a vector of parameter names, an expression to evaluate (which can contain let), and then a series of sets of arguments to be evaluated. In the following example, the parameter names vector is on the first line, the second and third line are the expression to evaluate, and the remaining lines are sets of arguments to assert. (Thus, the first set will plug in 1 for m1, 10 for s1,and "1:10" for expected and then test the expression with those values.)

(deftest duration-str
  (are [m1 s1 expected]
    (= (str (p/Duration. m1 s1) expected))
    1 10  "1:10"
    1 9 "1:09"
    1 60 "2:00"
    3 145 "5:25"
    0 0 "0:00")
Warning
You cannot use destructuring in the arguments to are, but you can use destructuring in a let within the expression you are testing. Also, when you save the test file, you may have to do the (require '[cljs.test :as t]) in the REPL again in order to try your tests again.

In this étude, you will write a series of tests for the rational and complex numbers. As you will note, some of the tests I used for durations were designed to try “edge cases” in the hopes of making the algorithms fail. Here are some of the things you might consider testing:

ExpressionExpected result
gcd(3, 5)1
gcd(12, 14)2
gcd(35, 55)5
1/2 + 1/35/6
2/8 + 3/121/2
0/4 + 0/50/20
1/0 + 1/00/0
6/8 - 6/121/4
1/4 - 3/4-1/2
1/3 * 1/41/12
3/4 * 4/31/1
1/3 ÷ 1/44/3
3/4 ÷ 4/39/16
(str (Complex. 3 7))"3+7i"
(str (Complex. 3 -7))"3-7i"
(str (Complex. -3 7))"-3+7i"
(str (Complex. -3 -7))"-3-7i"
(str (Complex. 0 7))"7i"
(str (Complex. 3 0))"3"
(1 + 2i) + (3 + 4i)4 + 6i
(1 - 2i) + (-3 + 4i)-2 + 2i
(1 + 2i) - (3 + 4i)-2 - 2i
(1 + 2i) * (3 + 4i)-5 + 10i
2i * (3 - 4i)8 + 6i
(3 + 4i) ÷ (1 + 2i)2.2 - 0.4i
(1 - 2i) ÷ (3 - 4i)0.44 -0.08i

See a suggested solution: Solution 7-3.