March 18, 2018
By: Wayne Dyck

Clojure thread-first macro

When I first started learning Clojure I read it's preferred to use the threading macros -> (thread-first) and ->> (thread-last) to make code more readable by removing heavy nesting. When code is heavily nested it is more difficult to see the data flow.

I remember wondering what would constitute, "heavy form nesting?" Surely it's not something I would ever do. As it turns out it's easy to accomplish by simply building up several different forms to achieve an end goal.

For example, given the following path variable,

(def path "")

our task is to parse this string and extract the file extension from the end of it.

Let's look at the build-up of successive forms as they might occur when developing in a REPL:

user=> (def path "")

user=> path

user=> (str/split path #"\.")
["https://www" "example" "com/site/default/files/MyAwesomeFile" "PDF#page=5"]

user=> (last (str/split path #"\."))

user=> (str/split (last (str/split path #"\.")) #"#")
["PDF" "page=5"]

user=> (first (str/split (last (str/split path #"\.")) #"#"))

user=> (str/lower-case (first (str/split (last (str/split path #"\.")) #"#")))

Now, let's compare the following code where we include the final result from our REPL development:

(def file-extensions #{"doc" "xls" "ppt", "pdf"})
(contains? file-extensions (str/lower-case (first (str/split (last (str/split path #"\.")) #"#"))))


(def file-extensions #{"doc" "xls" "ppt", "pdf"})
(contains? file-extensions (-> (str/split path #"\.")
                                (str/split #"#")

The first form uses several nested function calls and the second one makes use of the thread first -> macro.

The thread first -> macro passes its first argument as the first argument to the next form, then passes the result of that as the first argument to the next form and so on.

Though both forms achieve the same result, I find the second can be easier to grok at first glance once you understand what the thread first -> macro is doing.

Tags: Clojure