Tonλ's blog May the λ be with you

One way to solve a problem in clojure

by @ardumont on

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.

;.;. FAIL 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 parameters 1 8 and with the 1 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:

;.;. FAIL at (NO_SOURCE_FILE:1)
;.;. You claimed the following was needed, but it was never used:
;.;.     (mx 1 8 )
;.;.
;.;. FAIL at (NO_SOURCE_FILE:1)
;.;. You claimed the following was needed, but it was never used:
;.;.     (mx 1 30)
;.;.
;.;. FAIL 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, hit C-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,

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.

Latest posts