This is a series of small blog posts about following along the book 'The Little Schemer' using Clojure.
The book is great fun to read and it is easy to understand. The format is a kind conversation between the author and reader. And finally, the book even has dedicated pages for jelly-stains! It teaches programming small but realistic applications with the least amount of jargon and concepts. I think it is a perfect place to start learning to program if you haven't done so yet. It also helps seasoned developers like myself get back to the fundamentals of how to think about computation.
For me, the challenge was to follow along in the book via programming in Clojure, a recent JVM-based Lisp. I recently worked on a side-project where the most smartest solution would have been a recursive algorithm but I wasn't able to solve it without head-scratching all the way through. With this series I want to recap my recursion skills and learn them through Clojure, my favorite programming language.
I'm currently using a very barebones setup to follow along: I used Leiningen to create a standalone Clojure project `lein new the-little-clojurian`. I use GNU Emacs 25 with Cider on a 3-year old Macbook Air.
## Workflow
My workflow is as follows:
Once... 1. I open `project.clj` in a buffer and 2. run `M-x cider-jack-in` to get a REPL
Then, I... 1. Write the test as it appears in the book, 2. use `C-c k` to compile the file and 3. `C-c ,` to run the tests.
When I have a failing test, I fix the implementation only as much as to get the test to pass. Following the book, I continue this pattern until the chapter is over.
I am using the `with-test` macro and write my tests and implementation all in the main core namespace. Normally, we tend to write our tests in separate files but I've grown fond of `with-test` because of
1. less switching between buffers
2. having all code right in front of me.
I like it.
In a way, the book is written in a conversational, assumptions-first, almost test-driven way, so I write the assertion first and then gradually implement the function itself.
The function-definition, including tests for `lat?` looks like this for example:
```clojure
(ns the-little-clojurian.chapter2
(:require [clojure.test :refer :all]
[the-little-clojurian.chapter1 :refer :all]))
(with-test
;; Function-definition
(def lat?
(fn [l]
(cond (null? l) true
(atom? (car l)) (lat? (cdr l))
:else false)))
;; Tests
(testing "returns true"
(is (= (lat? '(Jack Sprat could eat no chicken fat)) true))
(is (= (lat? '()) true))
(is (= (lat? '(bacon and eggs)) true)))
(testing "returns false"
(is (= (lat? '((Jack) Sprat could eat no chicken fat)) false))
(is (= (lat? '(Jack (Sprat could) eat no chicken fat)) false))
(is (= (lat? '(bacon (and eggs))) false))))
```
The full transcript of Chapter 1 in Clojure is:
```clojure
(ns the-little-clojurian.chapter1
(:require [clojure.test :refer :all]))
(declare listp? atom? s-expression? car cdr conss null? eq?)
(with-test
(def atom?
(fn [a]
(not (listp? a))))
(testing "returns true"
(is (= true (atom? 'atom)))
(is (= true (atom? 'turkey)))
(is (= true (atom? '1492)))
(is (= true (atom? 'u)))
(is (= true (atom? '*abc$)))
(is (= (atom? 'Harry) true))
(is (= (atom? (car '(Harry had a heap of apples))) true))
(is (= (atom? (car (cdr '(swing low sweet cherry oat)))) true)))
(testing "returns false"
(is (= false (atom? '())))
(is (= false (atom? '(a b c))))
(is (= (atom? '(Harry had a heap of apples)) false))
(is (= (atom? (cdr '(Harry had a heap of apples))) false))
(is (= (atom? (car (cdr '(swing (low sweet) cherry oat)))) false))))
(with-test
(def listp?
(fn [s]
(list? s)))
(testing "returns true"
(is (= true (listp? '(atom))))
(is (= true (listp? '(atom? turkey or))))
(is (= true (listp? '((atom turkey) or))))
(is (= true (listp? '())))
(is (= true (listp? '(() () () ())))))
(testing "returns false"
(is (= false (listp? 'atom)))
(is (= false (listp? nil)))))
(with-test
(def s-expression?
(fn [x]
(not (nil? x))))
(testing "returns true"
(is (= true (s-expression? 'xyz)))
(is (= true (s-expression? '(x y z))))
(is (= true (s-expression? '((x y) z))))
(is (= true (s-expression? '(how are you doing so far))))
(is (= true (s-expression? '(((how) are) ((you) (doing so)) far)))))
(testing "returns false"
(is (= false (s-expression? nil)))))
(with-test
(def car
(fn [x]
(first x)))
(testing "returns s-expression"
(is (= 'a (car '(a b c))))
(is (= '(a b c) (car '((a b c) x y z))))
(is (= '((hotdogs)) (car '(((hotdogs)) (and) (pickle) relish))))
(is (= '(hotdogs) (car (car '(((hotdogs)) (and)))))))
(testing "returns nil"
(is (nil? (car '()))))
(testing "throws Exception"
(is (thrown? IllegalArgumentException (car 'hotdog)))))
(with-test
(def cdr
(fn [l]
(rest l)))
(testing "returns list"
(is (= '(b c) (cdr '(a b c))))
(is (= '(x y z) (cdr '((a b c) x y z))))
(is (= '() (cdr '(hamburger))))
(is (= '(t r) (cdr '((x) t r))))
(is (= '() (cdr '())))
(is (= (cdr nil) '())))
(testing "throws Exception"
(is (thrown? IllegalArgumentException (cdr 'hotdogs)))))
(deftest car-and-cdr-tests
(is (= '(x y) (car (cdr '((b) (x y) (()))))))
(is (= '(((c))) (cdr (cdr '((b) (x y) ((c)))))))
(is (thrown? IllegalArgumentException (cdr (car '(a (b (c)) d))))))
(with-test
(def conss
(fn [a l]
(cons a l)))
(testing "returns the consed list"
(is (= '(peanut butter and jelly) (conss 'peanut '(butter and jelly))))
(is (= '((banana and) peanut butter and jelly) (conss '(banana and) '(peanut butter and jelly))))
(is (= '(((help) this) is very ((hard) to learn)) (conss '((help) this) '(is very ((hard) to learn)))))
(is (= '((a b (c))) (conss '(a b (c)) '())))
(is (= '(a) (conss 'a '()))))
(testing "throws Exception"
(is (thrown? IllegalArgumentException (conss '() 'a)))
(is (thrown? IllegalArgumentException (conss 'a 'b)))))
(deftest cons-and-car-and-cdr-tests
(is (= '(a b) (conss 'a (car '((b) c d)))))
(is (= '(a c d) (conss 'a (cdr '((b) c d))))))
(with-test
(def null?
(fn [x]
(empty? x)))
(testing "returns true"
(is (= true (null? '())))
(is (= (null? (quote ())) true)))
(testing "returns false"
(is (= (null? '(a b c)) false)))
(testing "throws Exception"
(is (thrown? IllegalArgumentException (null? 'spaghetti)))))
(with-test
(def eq?
(fn [x y]
(if (or
(and (atom? x)
(listp? y))
(and (listp? x)
(atom? y)))
(throw (IllegalArgumentException.))
(= x y))))
(testing "returns true"
(is (= (eq? 'Harry 'Harry) true))
(is (= (eq? '() '(strawberry))) true)
(is (= (eq? (car '(Mary had a little lamb)) 'Mary)))
(is (= (eq? (car '(beans beans)) (car (cdr '(beans beans)))) true)))
(testing "returns false"
(is (= (eq? 'margarine 'butter) false))
(is (= (eq? 6 7)) false))
(testing "throws Exception"
(is (thrown? IllegalArgumentException (eq? (cdr '(soured milk)) 'milk)))
(is (thrown? IllegalArgumentException (eq? 'milk (cdr '(soured milk)))))))
```