Clojure Koans 25 Threading Macros Notebook

Original Clojure Koans: https://github.com/functional-koans/clojure-koans/

Please note there are spoilers for the Clojure Koans below.

{:deps {org.clojure/clojure {:mvn/version "1.10.1"}
        ;; complient is used for autocompletion
        ;; add your libs here (and restart the runtime to pick up changes)
        compliment/compliment {:mvn/version "0.3.9"}}}
deps.edn
Extensible Data Notation
{:hello (clojure-version)}
0.1s
Clojure

Initial Thoughts

Soon after starting to learn Clojure I bumped into thread-first and thread-last. Again, the allure of more readable code is quite enticing. For some reason, I'm confused about what exactly is going on, top to bottom, left to right. I hope to clear up this confusion in this Koans section.

(def a-list
    '(1 2 3 4 5))
(def a-list-with-maps
    '({:a 1} {:a 2} {:a 3}))
(defn function-that-takes-a-map [map a b]
    (get map :a))
(defn function-that-takes-a-coll [a b coll]
    (map :a coll))
Clojure
;; "We can use thread first for more readable sequential operations"
(= {:a 1}
   (-> {}
       (assoc :a 1)))
;; skill 25001: Create a sequence of readable sequential operations by using "thread first" (->)
;; question 25001: What exactly is happening here?
;; question 25002: What is the context of this exercise?
;; question 25003: What is this code more readible than? (what would it look like without thread first?) !!!
Clojure
;; "Consider also the case of strings"
(= "Hello world, and moon, and stars"
   (-> "Hello world"
       (str ", and moon")
       (str ", and stars")))
;; description: The string "Hello world", along with two function calls to `str` are passed to the thread-first macro such that, firstly the string literal ", and moon" is concatenated to "Hello world", and then that result is then "passed"/"threaded" to have the string literal ", and stars" concatenated to it for a final result of "Hello world, and moon, and stars". To visually see what is going on, commas may be inserted where the first parameter would go:
;;    (-> "Hello world"
;;        (str ,,, ", and moon")
;;        (str ,,, ", and stars"))
;;    Note: This is also valid code because commas are whitespace in Clojure.
;;    Source: https://clojure.org/guides/threading_macros
;; question 25004: What exactly is happening here?
;; question 25004a: See description and example above.
;; question 25005: How is "thread first" typically/usually described in its usage?
;; answer 25005a: From clojure.org/guides/threading_macros: "Taking an initial value as its first argument, -> threads it through one or more expressions."
;; skill 25002: Pass an argument through a list of function calls by using the "thread first" macro
Clojure
;; "When a function has no arguments to partially apply, just reference it"
(= "String with a trailing space"
   (-> "String with a trailing space "
       clojure.string/trim))
;; description: Using the "thread first" macro, a string is threaded through (?) the `trim` function
;; skill 25003: Thread an argument through a function reference by using the "thread first" macro 
;; question 25006: What does it mean to "just reference it"?
;; answer 25006a: By "just reference it" I believe the author is indicating that they are using the function itself, rather than its invocation/call.
;; question 25007: What is meant by "partially apply" above?
Clojure
;; "Most operations that take a scalar value as an argument can be threaded-first"
(= 6
   (-> {}
       (assoc :a 1)
       (assoc :b 2)
       (assoc :c {:d 4
                  :e 5})
       (update-in [:c :e] inc)
       (get-in [:c :e])))
;; description: Add key-val :a 1, add key-val :b 2, add key-val :c {:d 4 :e 5}. Next, update the key-val :e inside :c by applying `inc`, and then get the key-value of :e inside :c.
;; question 25008: What are example scalar values in Clojure?
;; question 25009: What are example non-scalar values in Clojure?
Clojure
;; "We can use functions we have written ourselves that follow this pattern"
(= 1
   (-> {}
       (assoc :a 1)
       (function-that-takes-a-map "hello" "there")))
;; question 25010: What exactly is happening here?
;; question 25011: What are the role/function "function-that-takes-a-map", "hello", and "there" in this exercise?
Clojure
;; "We can also thread last using ->>"
(= [2 3 4]
   (->> [1 2 3]
        (map inc)))
;; skill 25004: Pass a function to be applied over a collection by using "thread last" (->>)
;; exercise 25001: Increment a collection of numbers by using `->>` (thread last)
Clojure
;; "Most operations that take a collection can be threaded-last"
(= 12
   (->> a-list
        (map inc)
        (filter even?)
        (into [])
        (reduce +)))
;; skill 25005: Thread a collection through a thread-last macro with pre-built core Clojure functions
;; description: The list "a-list" is threaded last through a list of functions, where each function is expecting an argument at the final position in their argument lists. Illustrated visually, it would appear like so:
;; (->> a-list
;;      (map inc ,,,)
;;      (filter even? ,,,)
;;      (into [] ,,,)
;;      (reduce + ,,,))
;; step-by-step: '(1 2 3 4 5) -> '(2 3 4 5 6) -> '(2 4 6) -> [2 4 6] -> 12
Clojure
;; "We can use functions we have written ourselves that follow this pattern"
(= [1 2 3]
   (->> a-list-with-maps
        (function-that-takes-a-coll "hello" "there")
        (into [])))
;; question 25012: What exactly is happening here?
;; skill 25006: Thread a collection through a thread-last macro with custom functions
;; notes:
;;    a-list-with-maps: '({:a 1} {:a 2} {:a 3})
;;    function-that-takes-a-coll: (map :a coll)
Clojure

Closing Thoughts

I feel much more confident about thread-first and thread-last, in both the knowledge of what they do, and how I can use them in my programs.

Both thread-first and thread-last run from "top to bottom", in that they "pass" the initial value (the top-most / first argument) sequentially. The words "first" and "last" are there to indicate where the next function in the sequence will accept the result of the prior function. In other words, "thread-first" gives the passed item to functions as the first argument. "thread-last" on the other hand takes the passed item to give to its functions as the last argument. If you missed it, take a look at the "code illustrations" up above that use three commas (,,,) to indicate how & where the "threaded argument" is being passed.

As a final note, I'd like to add that I have some experience with "dot chaining" (a functional programming technique) higher order functions in JavaScript and TypeScript, so I had some prior related experience to draw on and help me understand more quickly what was happening here in Clojure.

I definitely see the value in thread-first and thread-last, and I can't wait to try and apply it to a coding study in the near future.

;; making sure I understand how to split a string...
(clojure.string/split "Hi there" #" ")
0.0s
Clojure
(defn rm-blanks [s]
  (filter #(not (empty? %)) s))
(defn join-with-spaces [s]
  (clojure.string/join #" " s))
0.1s
Clojure
(-> "  I am not too sleepy today   , fortunately    ..."
  clojure.string/trim ;; this is OK too: (clojure.string/trim)
  (clojure.string/split #" ")
  rm-blanks
  join-with-spaces
  (clojure.string/replace " ," ","))
;; description: This thread-first macro will remove trailing and leading spaces, split on spaces into a vector of words and punctuation, remove any empty strings, glue the vector back into a list, and then remove any whitespace preceding a comma.
;; skill 25007: [epic skill] Thread a combination of function references, function calls, core library functions, and custom functions together in a thread-first macro to modify a sequence/collection
;; gotcha: Functions that only take one argument need not be wrapped in parentheses when threaded. Functions that take more than one argument MUST (?) be wrapped in parentheses.
0.0s
Clojure
Runtimes (1)