Quick Clojure Review
March 1, 2020
Clojure is a functional, symbiotic, and homoiconic programming language. - Functional: where functions are first-class citizens and mutating state is frowned upon - Symbiotic: the language is intended to be run atop a host. environment - Homoiconic: “code is data” — this helps facilitate a macro system for rewriting the language.
Data Structures
Clojure provides a language API based upon a select set of data structures. - List: (1 2 3) - Vector: [1 2 3] - Map: {:foo “bar”} - Set: #{1 2 3}
List
A list uses parentheses as its surrounding delimiters, and so an empty list would look like (), whereas a list with three elements could look like (“a” “b” “c”).
Clojure will happily hide those details away from you and provide abstractions that make dealing with data structures in the most appropriate and performant manner very easy.
By using the cons function, which will insert your value at the beginning of the list.
Or, conj function instead, which will pick the correct method for inserting the new value at the start of the list.
But, if your data structure were a vector collection instead, then the conj function would know to insert the value at the end of the collection.
There are other abstraction functions:
Vector
Vectors allow you to have index access to any element within the data structure.
You can modify the vector by using the assoc function (which is an abbreviation of “associate”). The way it works is that you provide the index of the vector you want to modify and then provide the value.
but what if you want to remove a value? One way to do this would be to use the pop function, which returns a copy of the vector but with the last element removed.
Map
The map data structure goes by many different names—hash, hash map, dictionary—and what distinguishes it from other data structures is the underlying implementation, which is a key part of ensuring the algorithmic performance of this particular data structure.
{:my-key "this is my value"}
(get {:my-key "this is my value"} :my-key)
;; "this is my value"
;; If you want the entire entry
(find {:a 1 :b 2} :a)
;; [:a 1]
(assoc {:foo "bar"} :baz "qux")
;; {:foo "bar", :baz "qux"}
(dissoc {:foo "bar" :baz "qux"} :baz)
;; {:foo "bar"}
(select-keys {:name "Mark" :age 33 :location "London"} [:name :location])
;; {:name "Mark", :location "London"}Keywords
Some readers may be wondering what the colon prefixing the key is supposed to mean. The colon indicates that the key is actually a keyword.
;; Keyword as a Function
(get {:foo "bar" :baz "qux"} :baz)
;; "qux"
(:baz {:foo "bar" :baz "qux"})
;; "qux"
;; Demonstrate the contains? Function
(contains? {:foo "bar" :baz "qux"} :foo)
;; trueKeys, Values, and Replacement
;; Demonstrate the keys and vals Functions
(keys {:foo "bar" :baz "qux"})
;; (:baz :foo)
(vals {:foo "bar" :baz "qux"})
;; ("qux" "bar")The replace function allows you to create a new vector consisting of values extracted from a map data structure.
;; Demonstrate the replace Function
(replace {:a 1 :b 2 :c 3} [:c :b :a])
;; [3 2 1]
(replace [:a :b :c] [2 1 0])
;; [:c :b :a].Set
A set is a data structure made up of unique values. Much like Clojure’s map and vector data structures, it provides Clojure with a very lightweight data model.
;; Simple Set Data Structure Example
#{1 2 3 :a :b :c}
;; #{1 :c 3 2 :b :a}
;; Filter Out Duplicates
(set [1 1 2 2 3 3 4 5 6 6])
;; #{1 4 6 3 2 5}
(apply sorted-set [1 1 2 2 3 3 4 5 6 6])
;; #{1 2 3 4 5 6}
;; Using conj to Add New Value to a Set
(conj #{1 2 3} 4)
;; #{1 4 3 2}
(conj #{1 2 3} 3)
;; #{1 3 2}
;; Remove Items from a Set with disj
(disj #{1 2 3} 3)
;; #{1 2}Functional Programming
- Immutability
- Referential transparency
- First-class functions
- Partial application
- Recursive iteration
- Composability
Immutability
If you have state and it can change, then once your application becomes distributed and concurrent, you’ll end up in a world of hurt, as many different threads can start manipulating your data at non-deterministic times. This can cause your application to fail at any given moment and become very hard to debug and to reason about. By offering immutability, Clojure can help to side-step this problem. In Clojure, every time you manipulate a data structure you are returned not a mutated version of the original, but rather a whole new copy with your change(s) applied. ## Referential transparency Referential transparency is when an expression can be replaced by its value without changing the behavior of a program.
The function sum (shown in Listing 3-1) is referentially transparent. No matter what happens, if I provide the same set of arguments (in this case 1 and 1), I’ll always get back the same result. ## First-class Functions For a language to offer “first-class functions,” it needs to be able to both store functions and pass functions around as if they were values. We’ve already seen the former being achieved using variables, and the latter (passing functions around as values) is also possible within Clojure. - complement - apply - map - reduce - filter - comp
complement
;; Example of the complement function returning the opposite truth value
((complement empty?) "")
;; falseapply
map
;; Example of map
(map inc [1 2 3])
;; (2 3 4)
;; The map Return Value Type Is a List
(map
(fn [[k v]] (inc v))
{:a 1 :b 2 :c 3}) ;; => (4 3 2)
;; (2 3 4)
;; Ensure map Returns Key/Value-like Data Structure
(map
(fn [[k v]] [k (inc v)])
{:a 1 :b 2 :c 3})
;; ([:c 4] [:b 3] [:a 2])reduce
filter
comp
;; Example of the comp Function
((comp clojure.string/upper-case (partial apply str) reverse) "hello")
;; "OLLEH"Partial application
Partial application helps to promote the creation of functions that can expand their use cases beyond their initial intent. The concept of partial application is regularly confused with another functional concept known as currying (which Clojure doesn’t support). When you “curry” a function, the function’s arguments are expanded internally into separate functions. A curried function won’t execute its body until all arguments have been provided (similar to partial application). So, again, if your function accepted three arguments you could effectively call your curried function in one of the following ways.
// Internal Representation of a Curry-Compiled Output
function f(a) {
function (b) {
function (c) {
return a + b + c;
}
}
}
foo('x')('y')('z') // 'xyz'So, just to recap, the main differences between currying and partial application are as follows. 1. You only partially apply your values once. So, if your function takes three arguments and you partially apply two of them, then when your resulting function is called you only provide one argument. If you had instead partially applied only one argument, you would still only call the resulting function once (but this time you would have to provide the remaining two arguments). 2. If we consider the “API” scenario from earlier, you are providing the initial values for the partially applied function, whereas with a curried function it is the user who provides the arguments.
Recursive Iteration
The classic for loop you’re likely familiar with for (i = 0; i < 10; i++) {} by design allows mutating local variables to increment the loop. In Clojure, local variables are immutable, and so for us to loop we need to use recursive function calls instead. Instead of looping, you’ll typically need to use the loop/recur special form, although a lot of the time other iterator-style functions such as map, reduce, and filter will be better fitted to solving the problem at hand. The main benefit of the loop/recur special form is that it allows you to safely apply recursive function calls without exhausting your memory stack. For example, if you’ve ever written any JavaScript code in your life you’ll likely have hit a problem at least once where you’ve exhausted the stack and caused a “stack overflow” error.
;; Example of Stack Exhaustion
(defn count-down [x]
(if (= x 0)
(prn "finished")
(count-down (do (prn x) (dec x)))))
(count-down 10) ;; works exactly as previous example BUT it's not safe!
(count-down 100000) ;; will cause a "StackOverflowError"Resolving the problem with the code will require a process that the else statement need to be modified so that instead of returning a function call to count-down you return a function.
;; Example of trampoline Function
(defn count-down [x]
(if (= x 0)
(prn "finished")
#(count-down (do (prn x) (dec x)))))
(trampoline count-down 10) ; works fine still
(trampoline count-down 100000) ; no longer triggers an errorRemember: #(…) is a shorthand syntax for an anonymous function. ## Composability The main reason this is such a key aspect of functional programming is that your units of functionality should be generic enough to be reused within many different contexts, rather than being overly specific to one environment and ultimately not being reusable.