In this article, we will see how to use the different tools at our disposal to solve a problem.
Notes
- Clojure is in no way limited to simple problems.
- The problem is not an impressive one as i want to focus on how to start a clojure project and how to use the tools.
Problem
Now to prove that we have all that we need, we will solve a little problem.
Write a function which takes a variable number of parameters and returns the maximum value.
Note There is already a max
function but we will forbid ourselves to use it.
To the solution in Top Down TDD
First fact
Let's design some facts to help us progress.
(fact (our-max 1 8 30) => 30)
Compilation problem
Compilation : C-c C-k
Here we will have a compilation problem as there is no function our-max
So add this before the fact:
(defn our-max "Given an infinite list of int parameters, compute the max of all the input integers." [& l]) (fact (our-max 1 8 30) => 30)
Explanations The declaration of the function:
- is declared with
defn
- followed by the name of the function we want, here
our-max
- then a doc-string to explicit what the function does
- the vector of arguments (in square-brackets as all the vectors). The
& l
means that the arguments is an indefinite number of parameters, that's exactly what we want! - and at last the body of the function (the implementation), here we did nothing.
Compilation ok, fact KO
Launch the compilation C-c C-k
.
Now, the compilation is ok, but we got fail facts.
That's ok because, with no implementation, we got a nil result.
;.;. [31mFAIL[0m at (NO_SOURCE_FILE:1) ;.;. Expected: 30 ;.;. Actual: nil (fact (our-max 1 8 30) => 30)
We expected 30 but got nil.
Note In clojure, a function always return something and in case of no implementation, we return nil.
Top Down Test Driven Development
We will try a Top Down TDD approach, i.e. we will make the function work but based on mock implementations of sub functions. And as soon as we have the top level done, we can develop the sub function we depend on.
So here, we can change the fact to depend on a mx
function (really a max
function) which computes the max between 2 integers.
(unfinished mx) (defn our-max "Our max implementation function" [& l]) (fact (our-max 1 8 30) => 30 (provided (mx 1 8 ) => 1 (mx 1 30) => 30))
explanations
- The
(unfinished list)
is here to tell midje, it's ok that you do not have the implementation yet. Do not fail the compilation for such a small detail. - We mock the call of a new function
mx
with the parameters1 8
and with the1 30
. - I voluntarily tell midje that the max between 1 and 8 is 1 for everybody to see that this is a mock implementation and in no way the reality.
By compiling this fact, Midje enriches its message for us:
;.;. [31mFAIL[0m at (NO_SOURCE_FILE:1) ;.;. You claimed the following was needed, but it was never used: ;.;. (mx 1 8 ) ;.;. ;.;. [31mFAIL[0m at (NO_SOURCE_FILE:1) ;.;. You claimed the following was needed, but it was never used: ;.;. (mx 1 30) ;.;. ;.;. [31mFAIL[0m at (NO_SOURCE_FILE:1) ;.;. Expected: 30 (fact (our-max 1 8 30) => 30 (provided (mx 1 8 ) => 1 (mx 1 30) => 30))
Basically, midje warns us about the absence of the mx
call in our implementation.
Indeed, we did not yet complete our implementation.
Note
- To add a not implemented function into the
unfinished list
, hitC-c u
First implementation, compilation ok, fact ok
The way i see it is this, we want to reduce
a list of elements to the max of its elements.
The function that does such a transformation is the reduce
function.
In the repl, type reduce
then C-c C-d d
, this will open a browser with the documentation on this function.
clojure.core/reduce ([f coll] [f val coll]) f should be a function of 2 arguments. If val is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc. If coll contains no items, f must accept no arguments as well, and reduce returns the result of calling f with no arguments. If coll has only 1 item, it is returned and f is not called. If val is supplied, returns the result of applying f to val and the first item in coll, then applying f to that result and the 2nd item, etc. If coll contains no items, returns val and f is not called.
This indeed is exactly what we want with mx
as our f
function and l
our coll
.
So here comes our implementation:
(defn our-max "Our max implementation function" [& l] (reduce mx l)) ;.;. Happiness comes when you believe that you have done something truly meaningful. -- Yan (fact (our-max 1 8 30) => 30 (provided (mx 1 8 ) => 1 (mx 1 30) => 30))
And this work!
Explanations
We want to compute the max in a list of integers, so we use reduce
to loop over the elements and compute the max.
The detailed step:
(mx 1 8 )
will give 1 according to the contract
(provided (mx 1 8 ) => 1)
(mx 1 30)
will give 30 according to the contract
(provided (mx 1 30) => 30)
- and that's it. The result is then
30
which indeed is what we expect.
So the fact is ok!
Next step: implement the mx
function.
mx
facts
It's just a max function, so here goes the facts:
(unfinished ) (defn mx "max" [x y] (if (< x y) y x)) ;.;. Without work, all life goes rotten. -- Camus (fact "mx" (mx 1 2) => 2 (mx 2 100) => 100)
Note The arity of the function (number of arguments) needed was 2 as we described in the first fact (our-max).
Final - Integration test
Now that we think we have everything, let's check it with a real fact (that is without mock).
For example, add this fact at the bottom of the file.
;.;. Out of clutter find simplicity; from discord find harmony; in the middle of difficulty lies opportunity. -- Einstein (fact (our-max 9786 4 7 87 9999 876 342 9876 999) => 9999)
Ok!
Code
Here is the final core.clj
file.
(ns hello.core (:use [midje.sweet])) ;; Write a function which takes a variable number of parameters and returns the maximum value. (unfinished ) (defn mx "max" [x y] (if (< x y) y x)) (fact "mx" (mx 1 2) => 2 (mx 2 100) => 100) (defn our-max "Our max implementation function" [& l] (reduce mx l)) (fact "mock our-max" (our-max 1 8 30) => 30 (provided (mx 1 8 ) => 1 (mx 1 30) => 30)) ;.;. Out of clutter find simplicity; from discord find harmony; in the middle of difficulty lies opportunity. -- Einstein (fact "ITest our-max" (our-max 9786 4 7 87 9999 876 342 9876 999) => 9999)
Conclusion
With these posts and this one,
- http://ardumont.github.io/howto-install-emacs24-with-some-mode/
- http://ardumont.github.io/howto-install-clojure/
- http://ardumont.github.io/howto-bootstrap-a-clojure-project
You now have all you need to develop with clojure.
For documentation about the different tool i used, i recommend reading the README on each github project which are really well explained.
Also, if you have some time, there is a good video from Brian Marick himself (midje's creator) using top down tdd to solve a more complex problem than this one.
In a near future, i intend to make some other blog posts to focus on:
- continued integration with travis-ci
- heroku for the deploying part
- marginalia for the documentation generation and the github integration.