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"
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.
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.
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.
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 deftest
s, 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"
)
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:
Expression | Expected result |
---|---|
gcd(3, 5) | 1 |
gcd(12, 14) | 2 |
gcd(35, 55) | 5 |
1/2 + 1/3 | 5/6 |
2/8 + 3/12 | 1/2 |
0/4 + 0/5 | 0/20 |
1/0 + 1/0 | 0/0 |
6/8 - 6/12 | 1/4 |
1/4 - 3/4 | -1/2 |
1/3 * 1/4 | 1/12 |
3/4 * 4/3 | 1/1 |
1/3 ÷ 1/4 | 4/3 |
3/4 ÷ 4/3 | 9/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.