Foray Into Clojure, Part 1: Collections, Conditionals, and Meditations | 隻手声あり、その声を聞け?

隻手声あり、その声を聞け? (Two hands clap and there is a sound. What is the sound of one hand?)

- 白隠 慧鶴 (Hakuin Ekaku)

Hakuin Ekaku self portrait

Continuing on the journey of knowledge, I wanted to practice more Clojure. Last time we went over first sections of Learn Clojure: syntax and functions. The main way I learned was by doing the exercises at the bottom of each page. But for the rest of the subjects in the guide, there are no exercises! :(

We will instead practice using a wonderful project called Clojure Koans. The gist of Clojure Koans is a bunch of failing assert calls. One needs to write the missing code to make the assert calls pass. Filling in the missing parts along with the context of the question and the “Koan” above it is how the Clojure Koans project suggests one should practice Clojure (and for me it’s learning, not just practicing).

What are “Koans”, though? Koans are riddles, stories, or puzzles that Zen Buddhists use to explore greater truths about the world. While I’m not a practicing member, I think it would be cool to intersperse some real Koans (like the one that started this post) and some relevant art pieces throughout the post. So I will! :)

If you want, you can read everything I have to say about Clojure by looking at the Clojure tag.

Table of Contents

Getting started with Clojure Koans

The README does a good job of explaining how to do the basic installation, but here’s the setup you WANT.

  1. Fork the repo to your own account so you can commit (and share) your changes.
  2. Clone the forked repo. For me: git clone git://github.com/TheCoreMan/clojure-koans.git
  3. Open spacemacs.
  4. Add the cloned repo as a known project to make stuff work nicely in projectile + helm by SPC SPC add-known and picking the cloned repo directory.
  5. Start your repl env by , '
  6. Open the first koans scroll: src/koans/01_equalities.clj.
  7. Open the repl to the side by typing , s a. (might take a while to install)
  8. Run the Koans by entering (exec "run") in the repl.

You should see something like this in your REPL:

clojure koan 1

Now, let’s fix this koan. This one’s pretty simple - write true instead of ___ and after writing the file here’s what you’ll see in the REPL:

clojure koans 2

As you can see, when you solve one Koan, the REPL shows you the next one! How neat is that? So now we are prepared to do the koans.

By the way, optional steps, but highly recommended: prepare yourself a cup of tea 🍵 and put on some chill music. Simpsonwave is not a joke!

Blogpost Structure

To avoid making this blogpost annoying by making you jump to GitHub to read the solutions, each section will include the solved Koan scroll in full. After the code, you can find my highlights. My solutions can by found on GitHub as well.

Also, spoiler alert! This blogpost includes the solution to the Koans. I wouldn’t worry too much about it though - I don’t find the solutions very “spoilable”.

Equalities

Two blind men crossing a bridge

(ns koans.01-equalities
  (:require [koan-engine.core :refer :all]))

(meditations
  "We shall contemplate truth by testing reality, via equality"
  (= true true)

  "To understand reality, we must compare our expectations against reality"
  (= 2 (+ 1 1))

  "You can test equality of many things"
  (= (+ 3 4) 7 (+ 2 5))

  "Some things may appear different, but be the same"
  (= true (= 2 2/1))

  "You cannot generally float to heavens of integers"
  (= false (= 2 2.0))

  "But a looser equality is also possible"
  (= true (== 2.0 2))

  "Something is not equal to nothing"
  (= true (not (= 1 nil)))

  "Strings, and keywords, and symbols: oh my!"
  (= false (= "hello" :hello 'hello))

  "Make a keyword with your keyboard"
  (= :hello (keyword "hello"))

  "Symbolism is all around us"
  (= 'hello (symbol "hello"))

  "What could be equivalent to nothing?"
  (= nil nil)

  "When things cannot be equal, they must be different"
  (not= :fill-in-the-blank 8))

not=

So, while not= may seem obvious, it was fun to use the tools I’ve learned last time to understand it completely:

koan-engine.runner> (doc not=)
-------------------------
clojure.core/not=
([x] [x y] [x y & more])
  Same as (not (= obj1 obj2))
nil
koan-engine.runner> (type not=)
clojure.core$not_EQ_
koan-engine.runner> (macroexpand not=)
#function[clojure.core/not=]
koan-engine.runner> (source not=)
(defn not=
  "Same as (not (= obj1 obj2))"
  {:tag Boolean
   :added "1.0"
   :static true}
  ([x] false)
  ([x y] (not (= x y)))
  ([x y & more]
   (not (apply = x y more))))

We can that the implementation of not= is 100% made out of things we’ve already learned about in the first blogpost, which is very cool - building blocks on building blocks on building blocks.

keyword & symbol

It’s interesting to see functions like keyword and symbol in this context. This is a great explanation of language internals, which I think is possible in Clojure specifically because of how “bootstrap”-i it is. Probably not immedietly useful, but cool.

Strings

A monk asked Zhàozhōu, “Does a dog have Buddha nature or not?” Zhaozhou said, “Wú”.

Portrait of Zhaozhou Congshen

(ns koans.02-strings
  (:require [koan-engine.core :refer :all]
            [clojure.string :as string]))

(meditations
  "A string is nothing more than text surrounded by double quotes"
  (= "hello" "hello")

  "But double quotes are just magic on top of something deeper"
  (= "world" (str 'world))

  "You can do more than create strings, you can put them together"
  (= "Cool right?" (str "Cool " "right?"))

  "You can even get certain characters"
  (= \C (get "Characters" 0))

  "Or even count the characters"
  (= 11 (count "Hello World"))

  "But strings and characters are not the same"
  (= false (= \c "c"))

  "What if you only wanted to get part of a string?"
  (= "World" (subs "Hello World" 6 11))

  "How about joining together elements in a list?"
  (= "123" (string/join '(1 2 3)))

  "What if you wanted to separate them out?"
  (= "1, 2, 3" (string/join ", " '(1 2 3)))

  "Maybe you want to separate out all your lines"
  (= ["1" "2" "3"] (string/split-lines "1\n2\n3"))

  "You may want to make sure your words are backwards"
  (= "olleh" (string/reverse "hello"))

  "Maybe you want to find the index of the first occurrence of a substring"
  (= 0 (string/index-of "hello world" "hell"))

  "Or maybe the last index of the same"
  (= 13 (string/last-index-of "hello world, hello" "hello"))

  "But when something doesn't exist, nothing is found"
  (= nil (string/index-of "hello world" "bob"))

  "Sometimes you don't want whitespace cluttering the front and back"
  (= "hello world" (string/trim "  \nhello world \t \n"))

  "You can check if something is a char"
  (= true (char? \c))

  "But it may not be"
  (= false (char? "a"))

  "But chars aren't strings"
  (= false (string? \b))

  "Strings are strings"
  (= true (string? "hello"))

  "Some strings may be blank"
  (= true (string/blank? ""))

  "Even if at first glance they aren't"
  (= true (string/blank? " \n \t  "))

  "However, most strings aren't blank"
  (= false (string/blank? "hello?\nare you out there?")))

What’s good about this Koan is that nothing is surprising or interesting in the string package, or how Clojure decides to work with strings, at all. Which is good.

You know you are working on clean code when each routine you read turns out to be pretty much what you expected.

- Ward Cunningham

Let’s take a deeper look into “require”

This is the first time I’m looking at a :require. There’s a great explanation about it in the relevant section of “Learn Clojure”, so start there and then we can break down this code:

  (:require [koan-engine.core :refer :all]
            [clojure.string :as string]))

The :require class does a few things here. First, load the koan-engine.core namespace, and refer all the Vars from that namespace using their unqualified names. That’s how the project knows what meditations is. Since we’re using the lein REPL here, we can use it to see into the meditations macro and see how the Koans project itself works using doc and source calls!

koan-engine.runner> (doc meditations)
-------------------------
koan-engine.core/meditations
([& forms])
Macro
nil

This is already interesting - we can see koan-engine.core/meditations which indicates that it indeed was required AND referred, since in the Koan scroll we refer to it simply as meditations. Cool! And how does it work? Well, one source meditations later:

;; koan-engine.runner> (source meditations)
(defmacro meditations [& forms]
  (let [pairs (ensure-valid-meditation (partition 2 forms))
        tests (map (fn [[doc# code#]]
                     `(u/fancy-assert ~code# ~doc#))
                   pairs)]
    `(do ~@tests)))
;; koan-engine.runner> (source ensure-valid-meditation)
(defn ensure-valid-meditation [doc-expression-pairs]
  (doseq [[doc expression] doc-expression-pairs]
    (when-not (string? doc)
      (throw (ex-info (str "Meditations must be alternating doc/expression pairs\n"
                           "Expected " doc " to be a documentation string")
                      {:line (:line (meta doc))}))))
  doc-expression-pairs)
;; koan-engine.runner> (doc u/fancy-assert)
;; -------------------------
;; koan-engine.util/fancy-assert
;; ([x] [x message])
;; Macro
;;  Assertion with fancy error messaging.
;; koan-engine.runner> (source u/fancy-assert)
(defmacro fancy-assert
  "Assertion with fancy error messaging."
  ([x] `(fancy-assert ~x ""))
  ([x message]
     `(try
        (safe-assert (= true ~x) ~message)
        (catch Throwable e#
          (throw (ex-info (str '~message "\n" '~x)
                          {:line (:line (meta '~x))}))))))

Feels like looking into the Matrix.

Seeing the matrix

The second thing the :require class does is it grabs clojure/string (which we use for string/blank? for example) and aliases it to string. Otherwise, we’d have to write clojure.string/blank? every time, which seems silly. Importing/requiring libraries and then aliasing the imported library is a must have feature that plenty of languages have; In Python it’s

import x as y

and in Go it’s

import (
	z "github.com/x/y"
)

Lists

逢佛殺佛 (If you meet the Buddha, kill him.)

- 临济义玄 (Linji Yixuan)

Japanese painting of Linji Yixuan (Jap. Rinzai Gigen)

(ns koans.03-lists
  (:require [koan-engine.core :refer :all]))

(meditations
  "Lists can be expressed by function or a quoted form"
  (= '(1 2 3 4 5) (list 1 2 3 4 5))

  "They are Clojure seqs (sequences), so they allow access to the first"
  (= 1 (first '(1 2 3 4 5)))

  "As well as the rest"
  (= '(2 3 4 5) (rest '(1 2 3 4 5)))

  "Count your blessings"
  (= 3 (count '(dracula dooku chocula)))

  "Before they are gone"
  (= 0 (count '()))

  "The rest, when nothing is left, is empty"
  (= '() (rest '(100)))

  "Construction by adding an element to the front is easy"
  (= '(:a :b :c :d :e) (cons :a '(:b :c :d :e)))

  "Conjoining an element to a list isn't hard either"
  (= '(:e :a :b :c :d) (conj '(:a :b :c :d) :e))

  "You can use a list like a stack to get the first element"
  (= :a (peek '(:a :b :c :d :e)))

  "Or the others"
  (= '(:b :c :d :e) (pop '(:a :b :c :d :e)))

  "But watch out if you try to pop nothing"
  (= "No dice!" (try
          (pop '())
          (catch IllegalStateException e
            "No dice!")))

  "The rest of nothing isn't so strict"
  (= '() (try
          (rest '())
          (catch IllegalStateException e
            "No dice!"))))

Quoted form

This is interesting and I understand it (I think) after learning syntax and talking about List vs Invokation a lot last time. Basically, you’d think that this is a list:

(1 2 3)

but what it means is an invokation. You’ll try to invoke “1” and since “1” is a symbol that’s not a callable type it won’t work. So how do we make lists?

'(1 2 3)

Calling conj on a list

Nice implementation here, conj(oin) to a list type will add it to the beginning of the list. Makes sense when you think about it, it would have been O(n) otherwise. Let’s look at the docs to be sure:

koan-engine.runner> 
(doc conj)
-------------------------
clojure.core/conj
([coll x] [coll x & xs])
  conj[oin]. Returns a new collection with the xs
    'added'. (conj nil item) returns (item).  The 'addition' may
    happen at different 'places' depending on the concrete type.

Vectors

Every time Baizhang, Zen Master Dahui, gave a dharma talk, a certain old man would come to listen. He usually left after the talk, but one day he remained. Baizhang asked, “Who is there?”

The man said, “I am not actually a human being. I lived and taught on this mountain at the time of Kashyapa Buddha. One day a student asked me, ‘Does a person who practices with great devotion still fall into cause and effect?’ I said to him, ‘No, such a person doesn’t.’ Because I said this I was reborn as a wild fox for five hundred lifetimes. Reverend master, please say a turning word for me and free me from this wild fox body.” Then he asked Baizhang, “Does a person who practices with great devotion still fall into cause and effect?”

Baizhang said, “Don’t ignore cause and effect.”

Immediately the man had great realization. Bowing, he said, “I am now liberated from the body of a wild fox. I will stay in the mountain behind the monastery. Master, could you perform the usual services for a deceased monk for me?”

Baizhang asked the head of the monks’ hall to inform the assembly that funeral services for a monk would be held after the midday meal. The monks asked one another, “What’s going on? Everyone is well; there is no one sick in the Nirvana Hall.” After their meal, Baizhang led the assembly to a large rock behind the monastery and showed them a dead fox at the rock’s base. Following the customary procedure, they cremated the body.

That evening during his lecture in the dharma hall Baizhang talked about what had happened that day. Huangbo asked him, “A teacher of old gave a wrong answer and became a wild fox for five hundred lifetimes. What if he hadn’t given a wrong answer?”

Baizhang said, “Come closer and I will tell you.” Huangbo went closer and slapped Baizhang’s face. Laughing, Baizhang clapped his hands and said, “I thought it was only barbarians who had unusual beards. But you too have an unusual beard!”

Baizhang Huaihai

(ns koans.04-vectors
  (:require [koan-engine.core :refer :all]))

(meditations
  "You can use vectors in clojure as array-like structures"
  (= 1 (count [42]))

  "You can create a vector from a list"
  (= [1] (vec '(1)))

  "Or from some elements"
  (= [nil nil] (vector nil nil))

  "But you can populate it with any number of elements at once"
  (= [1 2] (vec '(1 2)))

  "Conjoining to a vector is different than to a list"
  (= [111 222 333] (conj [111 222] 333))

  "You can get the first element of a vector like so"
  (= :peanut (first [:peanut :butter :and :jelly]))

  "And the last in a similar fashion"
  (= :jelly (last [:peanut :butter :and :jelly]))

  "Or any index if you wish"
  (= :jelly (nth [:peanut :butter :and :jelly] 3))

  "You can also slice a vector"
  (= [:butter :and] (subvec [:peanut :butter :and :jelly] 1 3))

  "Equality with collections is in terms of values"
  (= (list 1 2 3) (vector 1 2 3)))

Calling conj on a vector

Ha! See? Told you.

Subvec inclusive or exclusive?

This is quite confusing (off-by-one things usually are), but subvec is both exclusive and inclusive at the same time:

koan-engine.runner> (doc subvec)
-------------------------
clojure.core/subvec
([v start] [v start end])
  Returns a persistent vector of the items in vector from
  start (inclusive) to end (exclusive).  If end is not supplied,
  defaults to (count vector). This operation is O(1) and very fast, as
  the resulting vector shares structure with the original and no
  trimming is done.

I believe this is done so that (count vector) will work nicely with it. As long as things are consistent, everything’s OK. It’s just always annoying to remember which number should I put where. And don’t get me started on 1-based languages like Lua.

Sets

How steep is Yün-mên’s mountain!

How low the white clouds hang!

The mountain stream rushes so swiftly

That fish cannot venture to stay.

One’s coming is well-understood

From the moment one steps in the door.

Why should I speak of the dust

On the track that is worn by the wheel?

— Yun-men, from the Jingde Chuandeng Lu 《景德傳燈錄》

Yun men's mountain

(ns koans.05-sets
  (:require [koan-engine.core :refer :all]
            [clojure.set :as set]))

(meditations
  "You can create a set by converting another collection"
  (= #{3} (set [3 3 3]))

  "Counting them is like counting other collections"
  (= 3 (count #{1 2 3}))

  "Remember that a set is a *mathematical* set"
  (= #{1 2 3 4 5} (set '(1 1 2 2 3 3 4 4 5 5)))

  "You can ask clojure for the union of two sets"
  (= #{1 2 3 4 5} (set/union #{1 2 3 4} #{2 3 5}))

  "And also the intersection"
  (= #{2 3} (set/intersection #{1 2 3 4} #{2 3 5}))

  "But don't forget about the difference"
  (= #{1 4} (set/difference #{1 2 3 4 5} #{2 3 5})))

This one was very simple, just good to know the #{} syntax.

Maps

Monk: “What is the one road of Ummon?”

Ummon: “Personal Experience!”

Monk: “What is the Way?”

Ummon: “Go!”

Monk: “What is the road, where is the Way?”

Ummon: “Begin walking it!”

yunman

(ns koans.06-maps
  (:require [koan-engine.core :refer :all]))

(meditations
  "Don't get lost when creating a map"
  (= {:a 1 :b 2} (hash-map :a 1 :b 2))

  "A value must be supplied for each key"
  (= {:a 1} (hash-map :a 1))

  "The size is the number of entries"
  (= 2 (count {:a 1 :b 2}))

  "You can look up the value for a given key"
  (= 2 (get {:a 1 :b 2} :b))

  "Maps can be used as functions to do lookups"
  (= 1 ({:a 1 :b 2} :a))

  "And so can keywords"
  (= 1 (:a {:a 1 :b 2}))

  "But map keys need not be keywords"
  (= "Sochi" ({2010 "Vancouver" 2014 "Sochi" 2018 "PyeongChang"} 2014))

  "You may not be able to find an entry for a key"
  (= nil (get {:a 1 :b 2} :c))

  "But you can provide your own default"
  (= :key-not-found (get {:a 1 :b 2} :c :key-not-found))

  "You can find out if a key is present"
  (= true (contains? {:a nil :b nil} :b))

  "Or if it is missing"
  (= false (contains? {:a nil :b nil} :c))

  "Maps are immutable, but you can create a new and improved version"
  (= {1 "January" 2 "February"} (assoc {1 "January"} 2 "February"))

  "You can also create a new version with an entry removed"
  (= {1 "January"} (dissoc {1 "January" 2 "February"} 2))

  "Create a new map by merging"
  (= {:a 1 :b 2 :c 3} (merge {:a 1 :b 2} {:c 3}))

  "Specify how to handle entries with same keys when merging"
  (= {:a 1 :b 2 :c 3} (merge-with + {:a 1 :b 1} {:b 1 :c 3}))

  "Often you will need to get the keys, but the order is undependable"
  (= (list 2010 2014 2018)
     (sort (keys { 2014 "Sochi" 2018 "PyeongChang" 2010 "Vancouver"})))

  "You can get the values in a similar way"
  (= (list "PyeongChang" "Sochi" "Vancouver")
     (sort (vals {2010 "Vancouver" 2014 "Sochi" 2018 "PyeongChang"})))

  "You can even iterate over the map entries as a seq"
  (= {:a 2 :b 3}
     (into {}
           (map
            (fn [[k v]] [k (inc v)])
            {:a 1 :b 2}))))

Using maps as functions

So, maps are callable, and when you call them, you can do a lookup. This is an interesting way to do things:

koan-engine.runner> ({} :a)
nil
koan-engine.runner> ({:a 1} :a)
1

Immutability and assoc

In the “immutability” section the the “Learn Clojure” guide we learn that clojure collections are immutable and compared by value. So any function that changes a collection, actually returns a new collection. With that in mind, let’s take a look at assoc’s documentation:

koan-engine.runner> (doc assoc)
-------------------------
clojure.core/assoc
([map key val] [map key val & kvs])
  assoc[iate]. When applied to a map, returns a new map of the
    same (hashed/sorted) type, that contains the mapping of key(s) to
    val(s). When applied to a vector, returns a new vector that
    contains val at index. Note - index must be <= (count vector).
nil
koan-engine.runner> 

And let’s test:

koan-engine.runner> (def mymap {:a 1})
#'koan-engine.runner/mymap
koan-engine.runner> (println mymap)
{:a 1}
nil
koan-engine.runner> (assoc mymap :b 2 :c 3)
{:a 1, :b 2, :c 3}
koan-engine.runner> (println mymap)
{:a 1}
nil
koan-engine.runner> 

merge-with

Seems like a really strong function! I’ve been developing some Gremlin code at work recently, in Python. Something like this has been sorely missing. In fact, I’ve even talked about it in the Gremlin Google Group. It’s great to have another example at hand.

Scary looking functional code

I don’t easily grok functional code. Usually I have to go over it a few times until I understand it, since it’s really not my background. For example, this is scary to me:

  "You can even iterate over the map entries as a seq"
  (= {:a 2 :b 3}
     (into {}
           (map
            (fn [[k v]] [k (inc v)])
            {:a 1 :b 2}))))

So let’s face our fears and understand what it means!

map takes a function and a collection, and, according to the docs, returns a lazy sequence of the result of applying the function to the items in the collection. Our function takes a vector of two elements and returns the same vector with the second element incremented. Wait, so are map key-value pairs actually just vectors? Let’s check:

koan-engine.runner> (first {:a 1 :b 2})
[:a 1]
koan-engine.runner> (type (first {:a 1 :b 2}))
clojure.lang.MapEntry

Aha! So, no? Or wait - maybe? It definitely looks like a vector to me. Turns out that both the MapEntry and PersistentVector implement APersistentVector so a MapEntry is a vector. Sort of.

Now, into. From the relevant part of “Learn Clojure”, we learn that into is used to put one collection into another, returning the type of the first. Here, we’re putting the result of the map call into an empty map. OK! So that’s how the “vectors” are casted back into MapEntries.

mischief managed

Functions

A monk once asked Ummon, “What is this place where knowledge is useless?”

Ummon answered him: “Knowledge and emotion cannot fathom it!”

yunman

(ns koans.07-functions
  (:require [koan-engine.core :refer :all]))

(defn multiply-by-ten [n]
  (* 10 n))

(defn square [n] (* n n))

(meditations
  "Calling a function is like giving it a hug with parentheses"
  (= 81 (square 9))

  "Functions are usually defined before they are used"
  (= 20 (multiply-by-ten 2))

  "But they can also be defined inline"
  (= 10 ((fn [n] (* 5 n)) 2))

  "Or using an even shorter syntax"
  (= 60 (#(* 15 %) 4))

  "Even anonymous functions may take multiple arguments"
  (= 15 (#(+ %1 %2 %3) 4 5 6))

  "Arguments can also be skipped"
  (= "AACC" (#(str "AA" %2) "bb" "CC"))

  "One function can beget another"
  (= 9 (((fn [] +)) 4 5))

  "Functions can also take other functions as input"
  (= 20 ((fn [f] (f 4 5))
           (fn [x y] (* x y))))

  "Higher-order functions take function arguments"
  (= 25 ((fn [f] (f 5))
          (fn [n] (* n n))))

  "But they are often better written using the names of functions"
  (= 25 ((fn [f] (f 5)) square)))

Begetting functions from functions

This might be just syntactic suger, but using `(fn [] +) to return the add operation as a symbol seems powerful.

Higher order functions

What are higher order functions? Well, there’s a Clojure guide about them! They are functions that take other functions as arguments, which is obvious very important for functional programming. We’ve seen one higher order function already - map. In this Koan we wrote a higher order function of our own:

(fn [f] (f 5)

It gets a function as an argument and calls that function with 5.

There’s a whole Koan scroll about higher order functions later on, so let’s put a 📌 in it for now and carry on.

Conditionals

Said Ummon to his disciples, “I do not ask you to say anything about before the fifteenth day of the month, but say something about after the fifteenth day of the month.”

Because no monk could reply, Ummon answered himself and said, “日々是好日!” (“Every day is a good day!”)

yunmen

(ns koans.08-conditionals
  (:require [koan-engine.core :refer :all]))

(defn explain-exercise-velocity [exercise-term]
  (case exercise-term
        :bicycling        "pretty fast"
        :jogging          "not super fast"
        :walking          "not fast at all"
        "is that even exercise?"))

(meditations
  "You will face many decisions"
  (= :a (if (false? (= 4 5))
          :a
          :b))

  "Some of them leave you no alternative"
  (= [] (if (> 4 3)
          []))

  "And in such a situation you may have nothing"
  (= nil (if (nil? 0)
          [:a :b :c]))

  "In others your alternative may be interesting"
  (= :glory (if (not (empty? ()))
              :doom
              :glory))

  "You may have a multitude of possible paths"
  (let [x 5]
    (= :your-road (cond (= x 1) :road-not-taken
                        (= x 2) :another-road-not-taken
                        :else :your-road)))

  "Or your fate may be sealed"
  (= 'doom (if-not (zero? 3)
          'doom
          'more-doom))

  "In case of emergency, go fast"
  (= "pretty fast"
     (explain-exercise-velocity :bicycling))

  "But admit it when you don't know what to do"
  (= "is that even exercise?"
     (explain-exercise-velocity :watching-tv)))

Conditionals are expressions, so they return a value

This is 🤯 to me. It does read very nicely, even if it’s vastly different from almost every other language I’ve used. In a general sense, what this does is force the developer to make their conditionals be used for getting values, not for choosing long-winding execution branches. According to CRs I’ve done, and Dijkstra’s laws, and plenty of other intuition, this seems… really good?

Well, good in the sense that exercise, sleep and eating well are really good. It’s really good but I hate doing it.

Higher order functions

Ummon Zenji said: “Men of immeasurable greatness are tossed about in the ebb and flow of words.”

landscape with pavilion

(ns koans.09-higher-order-functions
  (:require [koan-engine.core :refer :all]))

(meditations
  "The map function relates a sequence to another"
  (= [4 8 12] (map (fn [x] (* 4 x)) [1 2 3]))

  "You may create that mapping"
  (= [1 4 9 16 25] (map (fn [x] (* x x)) [1 2 3 4 5]))

  "Or use the names of existing functions"
  (= [false false true false false] (map nil? [:a :b nil :c :d]))

  "A filter can be strong"
  (= '() (filter (fn [x] false) '(:anything :goes :here)))

  "Or very weak"
  (= '(:anything :goes :here) (filter (fn [x] true) '(:anything :goes :here)))

  "Or somewhere in between"
  (= [10 20 30] (filter (fn [x] (< x 35)) [10 20 30 40 50 60 70 80]))

  "Maps and filters may be combined"
  (= [10 20 30] (map (fn [x] (* x 10)) (filter (fn [x] (< x 4)) [1 2 3 4 5 6 7 8])))

  "Reducing can increase the result"
  (= 24 (reduce (fn [a b] (* a b)) [1 2 3 4]))

  "You can start somewhere else"
  (= 2400 (reduce (fn [a b] (* a b)) 100 [1 2 3 4]))

  "Numbers are not the only things one can reduce"
  (= "longest" (reduce (fn [a b]
                         (if (< (count a) (count b)) b a))
                       ["which" "word" "is" "longest"])))

Map Reduce - Name a more iconic duo

I’ve been working a lot with high-scale data pipelines using Spark recently (at work). I still can’t really wrap my head around how these frameworks and these “map reduce” functions are basically the same. My brain isn’t wired to deal with such huge differences in scale!

Apache spark

Using Filter Map and Reduce in day-to-day work is very useful, but sometimes harder to read, as this reoccurring Twitter drama shows:

Closing words

Next time, we’ll either continue with the Koans - they are super fun, and a very engaging project - or start actually developing a script that does something real with Clojure. In any case, learning Clojure is slowly starting to become more fun as I can understand the glyphs in front of my eyes. Functional programming is also a good way to feel smart :)

While I’m writing these posts half for their documentational value and half just for myself as a personal technical journal, feel free to reach out via Twitter/LinkedIn if you’ve made it so far in the post and tell me what you think!


clojure

4781 Words

2021-06-04 20:39 +0300