The path of least resistance in Clojure
(Edit 2: Standard disclaimer: Clojure is a new hobby.)
Immutable data structures seem like an odd idea, but they are on the path of least resistance in Clojure. This is not the case in languages like Java. In this post I will rewrite a snippet of Java into Clojure and explain the major differences.
The snippet of Java comes from Martin Fowler's bliki posting on Fluent Interfaces. I picked this code snippet because it is concise, reads easily, and fills a common need.
customer.newOrder()
.with(6, "TAL")
.with(5, "HPK").skippable()
.with(3, "LGV")
.priorityRush();
}
This code clearly links an existing customer to a new, rush order of three items. Consider the "skippable" method... that is probably on the Order object yet works on an OrderLine. The Order code probably finds the newest OrderLine and updates its skippable property. Modifying objects is the path of least resistance in Java, or rather creating objects in an incomplete state and updating them until complete.
Modifying objects in Clojure is not on the path of least resistance. Many Clojure methods return a new, lazy version of the input. Working with return values like this encourages creating complete objects and then using them. Here's an example -- please temporarily ignore the unfamiliar functions and low level of abstraction.
(let [lines [(struct order-line 6, "TAL")
(struct-map order-line :quantity 5,
:code "HPK", :skippable? true)
(struct order-line 3, "LGV")]
order (struct-map order :lines lines,
:rush? true)]
(merge customer
{:orders (conj (get customer
:orders []) order)})))
- (struct foo 123 "abc") is basically calling the constructor for the foo class with two arguments.
- (struct-map foo :num 123, :text "abc") calls the constructor with named params rather than positional params
- (let) is how you define local variables, lines and order in the above example.
- (merge old-map new-map) creates a new map by merging existing maps
- (conj) creates a new collection from its arguments
- (get) returns the value for a key in a hash map, or a default value if specified.
It's not obvious but the return value is the output from (merge). That is the last statement in the (let) block, which is the only statement in the body of the (add-order) function. Values flowing like this is in the path of least resistance in Clojure.
Besides doing things in a different order than Java, there is one more very important but subtle difference. Let's say a customer wants two orders:
Some quick code to show how it works:
(def me (add-order (struct customer "seth")))
;; how many orders?
(count (me :orders)) ;; 1
;; I'd like another order, please...
(add-order me)
;; Hey, where'd that go?
(count (me :orders)) ;; 1
;; Is the function even working?
(count (:orders (add-order me))) ;; 2, so yes
The new order has been lost because the return value from the function was not captured. This is a silent runtime failure -- not a fun type of problem to chase down. One solution is to rebind "me":
(count (me :orders)) ;; 2
I've been a little dishonest here talking about "objects" in Clojure. As seen here they are little different than hash maps.
clojure.lang.PersistentArrayMap
example> (defstruct foo :bar)
#'example/foo
example> (. (struct foo) getClass)
clojure.lang.PersistentStructMap
For anyone who is interested, the entire source listing for this example follows. Thanks for reading!


