Teaching Clojure I
Functional programming is hype for the last ten years. You find a lot of blogs and videos stating object-oriented languages are doomed.
Almost all most popular languages have added some support to functional concepts in the last years.
Java introduced lambda expressions and single abstract method interfaces SAM with Java 8 in March 2014. Java developers are empowered to write functional code for almost ten years.
Other languages such as Python, C++ or C# added their own functional extensions in newer revisions.
Lisp dialects fully supported functional paradigms since the sixties of the last century.
The theory behind the functional model is \$lambda\$ calculus. Alonso Church formulated the lambda calculus theory in the thirties of the last century and millennium.
Why are functional programming paradigms suddenly so popular?
Functional languages have the following properties:
-
Functions are first-class objects. That is, functions can be created at runtime, passed around, returned, and in general, used like any other datatype.
-
Data is immutable.
-
Functions are pure; that is, they have no side effects.
OO Object Orientation makes code understandable by encapsulating moving parts.
FP Functional Programming makes code understandable by minimizing moving parts.
For many tasks, functional programs are easier to understand, less error-prone, and much easier to reuse.
Clojure Development Environment
Clojure [1] is a splendid approach to learn functional programming idioms. The language is well-documented.
The integration of the Java ecosystem and virtual machine is well-done.
Purely functional programs typically operate on immutable data.
Instead of altering existing values, altered copies are created and the original is preserved.
Since the unchanged parts of the structure cannot be modified, they can often be shared between the old and new copies, which saves memory.
The IntelliJ plugin Cursive hugely simplifies writing code and testing it [1]. IntelliJ IDEA is probably the most popular IDE for Java developers [2]. People are familiar with its functions and do not need to learn a new tool.
Declarations
You can define values and functions:
(defn square [param] (* param param)) (1)
(def data (list 1 2 3 4 5)) (2)
(def computed (map square data)) (3)
1 | Define defn a square function with one parameter and return the square value of the parameter. |
2 | Define def a list with five elements and store it the reference named data. |
3 | Apply the square method on all items of the data list. The computed list is stored in the reference named computed. |
Clojure provides extensions to validate type information.
(defn square [^Number param] (1)
{:pre [(number? param)]} (2)
(* param param))
1 | Define an optional type hint for the parameter with ^Number to state param must be a number. |
2 | Declare an optional precondition validating parameter values when the function is called. |
The equivalent constructs for local value and function declarations inside a function are:
(defn function-1 []
(let [local-variable-1 local-variable-value-1] (1)
body-of-let-block))
(defn function-2 [n]
(letfn [(do-local [a] (println a))] (2)
(do-local n)))
1 | The let expression declares local values in the scope of the function. Use it to minimize global name pollution. |
2 | The letfn expression declares local functions in the scope of the function. Use it to minimize global name pollution. |
Conditional Statements
You still need to write selection code. The usual constructs are:
(defn is-small? [number] (if (< number 100) "yes" "no")) (1)
(when (= 1 1) (+ 10 1)) (2)
(defn pos-neg-or-zero
"Determines whether or not n is positive, negative, or zero"
[n]
(cond (3)
(< n 0) "negative"
(> n 0) "positive"
:else "zero"))
1 | The expression if evaluates a boolean expression. If true returns the first expression, if false returns the optional second expression or nil if not defined. |
2 | The when expression is an if expression with only the true path. |
3 | The cond expression evaluates boolean conditions until the first one returning true is found. The associated expression is evaluated and the result returned. If no expression evaluates to true, the else expression is evaluated and returned or nil if not defined. |
Some Definitions
Symbols are identifiers that are normally used to refer to something else. They can be used in program forms to refer to function parameters, let bindings, class names, and global vars.
Keywords are symbolic identifiers that evaluate to themselves. They provide swift equality tests.
(char? (char 97)) ;; true
(char? 's) ;; false
(string? "This is a string") ;; true
(symbol? 'aSymbol) ;; true
(symbol? (symbol "aSymbol")) ;; false
(keyword? :aKeyword) ;; true
(keyword? (keyword "aKeyword")) ;; true
Arithmetic
Clojure supports all the number types of the Java language. Here are some examples:
(def aLong 42)
(def aDouble 42.42)
(def aBigInteger 42N)
(def aBigDecimal 42.42M)
(def aRatio 22/7) ;; => 22/7
Operator | Description | Example |
---|---|---|
even? |
Returns true if n is even |
(even? 2) returns true |
+ |
Addition of two operands |
(+ 1 2) returns 3 |
− |
Subtracts second operand from the first |
(- 2 1) returns 1 |
* |
Multiplication of both operands |
(* 2 2) returns 4 |
/ |
Division of numerator by denominator |
(float (/ 3 2)) returns 1.5 |
inc |
Increment the value of an operand by 1 |
(inc 5) returns 6 |
dec |
Decrement the value of an operand by 1 |
(dec 5) returns 4 |
max |
Returns the largest of its arguments |
(max 1 2 3) returns 3 |
min |
Returns the smallest of its arguments |
(min 1 2 3) returns 1 |
quot |
Returns the rounded division |
(quot 10 3) returns 3 |
rem |
Remainder of division |
(rem 3 2) returns 1 |
Study the documentation, more arithmetic and logical operators are available.
Collections
(def aList (list 1 2 3 4 5)) ;; => (1 2 3 4 5)
(def aList '(1 2 3 4 5)) ;; => (1 2 3 4 5)
(def aList (cons 1 (cons 2 (cons 3 (cons 4 (cons 5 ())))))) ;; => (1 2 3 4 5)
(first aList) ;; => 1
(rest aList) ;; => (2 3 4 5)
(def aMap {"Fred" 1400, "Bob" 1240}) ;; => {"Fred" 1400, "Bob" 1240}
(assoc aMap "Sally" 0) ;; => {"Fred" 1400, "Bob" 1240, "Sally" 0}
(dissoc aMap "Fred") ;; => {"Bob" 1240, "Sally" 0}
(get aMap "Bob") ;; => 1240
(aMap "Bob") ;; => 1240
(def aSet (set [:a :b :c :d])) ;; #{:a ;b :c :d}
(def aSet #{:a :b :c :d}) ;; #{:a ;b :c :d}
(count aSet) ;; 4
(aSet :b) ;; :b
All the Clojure collections are immutable and persistent. The Clojure collections support efficient creation of modified versions by utilizing structural sharing.
The collections are efficient and inherently thread-safe. Collections are represented by abstractions, and there may be one or more concrete realizations.
Links
-
[1] Teaching Clojure I. Marcel Baumann. 2023.
-
[2] Teaching Clojure II. Marcel Baumann. 2023.
-
[3] Clojure, Groovy, Java Marcel Baumann. 2022.
References
[1] S. D. Halloway, Programming Clojure. 2012 [Online]. Available: https://www.amazon.com/dp/B07BN4C92X