Clojure Syntax in 15 minutesλ︎
A quick tour of the Clojure syntax, which is so terse you can read through this section in around 15 minutes (or less).
This overview is expanded throughout the Practicalli Learning Clojure book along with practical examples.
Commentsλ︎
;;
two semi-colons for a line comment, ;
single semi-colon to comment the rest of the line
#_
comment reader macro to comment out the next form
(comment )
form to comment all the containing forms
Clojure written in formsλ︎
Clojure is written as "expressions", an expressions being a lists of elements (forms) inside parentheses, ()
, separated by a space character (or multiple spaces / lines).
Clojure evaluates the first element in an expression as a function call. Additional values in the expression are passed as arguments to the called function.
Clojure is organised into one or more namespaces. The namespace represents the directory path and file name that contains the code of the particular namespace.
;; Define the namespace test
(ns test.code) ;; src/test/code.clj
;; Define a longer namespace
(ns com.company.product-name.component.service) ;; src/com/company/product_name/component/service.clj
Kebab-case for Clojure, Snake-case for file_names
Lisps use kebab-case, words joined by -
, when names are composed of multiple words.
File and directory names must use snake_case, words joined by _
, as the underlying Java host does not support -
dash characters in names.
String manipulationλ︎
The str
function creates a new string from all the arguments passed
clojure.string
returns string values (other functions my return characters as results)
Math, Truth & prefix notationλ︎
Functions use prefix notation, so you can do math with multiple values very easily
Math is very precise, no need for operator precedence rules (as there are no operators)
Nesting forms defined a very precise calculation
Equality is =
true
and false
are Boolean values
clojure
(true? true) ; => true
(not true) ; => false
(not= true false) ; => true
(true? (complement true?)) ; => false
Typesλ︎
Clojure is strongly typed, so everything is a type in Clojure.
Clojure is dynamically typed, so Clojure infers the type. A type does not need to be specified in the code, making the code simpler and more concise.
As Clojure is a hosted language it uses the type system of its host where relevant. For example, Clojure uses Java object types for booleans, strings and numbers under the covers.
Use class
or type
functions to inspect the type of some code in Clojure.
(class 1) ; Integer literals are java.lang.Long by default
(class 1.); Float literals are java.lang.Double
(class ""); Strings always double-quoted, and are java.lang.String
(class false) ; Booleans are java.lang.Boolean
(class nil); The "null" value is called nil
Vectors and Lists are java classes too!
Collections & Sequencesλ︎
The most common data collections in Clojure:
(1 2 "three")
or(list 1 2 "three")
- a list of values read from start to end (sequential access)[1 2 "three"]
or(list 1 2 "three")
- a vector of values with index (random access){:key "value"}
or(hash-map :key "value")
- a hash-map with zero or more key value pairs (associative relation)#{1 2 "three"}
or(set 1 2 "three")
- a unique set of values
A list ()
is evaluated as a function call. The first element of the list the name of the function to call and additional values are arguments to the function.
The '
quote function informs the Clojure reader to treat the list as data only.
Lists and vectors are collections
Only lists are sequences
Sequences are an interface for logical lists, which can be lazy. "Lazy" means that a sequence of valus are not evaluated until accessed.
A lazy sequence enables the use of large or even an infinite series, like so:
(range) ; => (0 1 2 3 4 ...) - an infinite series
(take 4 (range)) ; (0 1 2 3) - lazyily evaluate range and stop when enough values are taken
Use cons to add an item to the beginning of a list or vector
Use conj to add an item relative to the type of collection, to the beginning of a list or the end of a vector
Use concat to add lists or vectors together
Use filter, map to interact with collections
Use reduce to reduce them
Reduce can take an initial-value argument too
Equivalent of (conj (conj (conj [] 3) 2) 1)
Functionsλ︎
Use fn
to create new functions that defines some behaviour. fn
is referred to as an anonymous fuction as it has no external name to be referenced by and must be called within a list form.
Wrap a (fn ,,,)
form in parens to call it and return the result
Create a reusable function using def
, creating a name that is a var
. The function behaviour defined in def
can be changed and the expression re-evaluated to use the new behaviour.
The [] is the list of arguments for the function.
Clojure supports multi-variadic functions, allowing one function definition to respond to a function call with different number of arguments
(defn hello3
([] "Hello World")
([name] (str "Hello " name)))
(hello3 "Jake") ; => "Hello Jake"
(hello3) ; => "Hello World"
Functions can pack extra arguments up in a seq for you
(defn count-args [& args]
(str "You passed " (count args) " args: " args))
(count-args 1 2 3) ; => "You passed 3 args: (1 2 3)"
You can mix regular and packed arguments
(defn hello-count [name & args]
(str "Hello " name ", you passed " (count args) " extra args"))
(hello-count "Finn" 1 2 3)
; => "Hello Finn, you passed 3 extra args"
Hash-map collectionsλ︎
Keywords are like strings with some efficiency bonuses
Maps can use any type as a key, but usually keywords are best
(def stringmap (hash-map "a" 1, "b" 2, "c" 3))
stringmap ; => {"a" 1, "b" 2, "c" 3}
(def keymap (hash-map :a 1 :b 2 :c 3))
keymap ; => {:a 1, :c 3, :b 2} (order is not guaranteed)
Commas are whitespace
commas are always treated as whitespace and are ignored by the Clojure reader
Retrieve a value from a map by calling it as a function
Keywords can be used to retrieve their value from a map. Strings cannot be used.
(:b keymap) ; => 2
("a" stringmap)
; => Exception: java.lang.String cannot be cast to clojure.lang.IFn
Retrieving a non-present value returns nil
Use assoc to add new keys to hash-maps
But remember, clojure types are immutable!
Use dissoc to remove keys
Setsλ︎
Add a member with conj
Remove one with disj
(disj #{1 2 3} 1) ; => #{2 3}
````
Test for existence by using the set as a function:
```clojure
(#{1 2 3} 1) ; => 1
(#{1 2 3} 4) ; => nil
There are more functions in the clojure.sets namespace.
Useful formsλ︎
Logic constructs in clojure are just macros, and look like everything else
Use let to create temporary bindings
Group statements together with do
Functions have an implicit do
(defn print-and-say-hello [name]
(print "Saying hello to " name)
(str "Hello " name))
(print-and-say-hello "Jeff") ;=> "Hello Jeff" (prints "Saying hello to Jeff")
So does let
(let [name "Urkel"]
(print "Saying hello to " name)
(str "Hello " name)) ; => "Hello Urkel" (prints "Saying hello to Urkel")
Namespaces and Librariesλ︎
Namespaces are used to organise code into logical groups. The top of each Clojure file has an ns
form that defines the namespace name. The domain part of the namespace name is typically the organisation or community name (e.g. GitHub user/organisation)
All Practicalli projects have namespace domains of practicalli
require
allows code from one namespace to be accessed from another namespace, either from a the same Clojure project or from a library added to the project classpath.
The :as
directive with require
is used to specify an alias name, a short-hand for the full library name
Or :refer [function-name var-name]
can be used to specify specific functions and data (vars) that are available directly
A required directive is typically added to a namespace form
The functions from clojure.set can be used via the alias name, rather than the fully qualified name, i.e. clojure.set/intersection
:require
directive can be used to include multiple library namespaces
require
can be used by itself, usually within a rich code block
Javaλ︎
Java has a huge and useful standard library, so you'll want to learn how to get at it.
Use import to load a java package
Or import from a java package name
Use the class name with a "." at the end to make a new instance
Use .
to call methods. Or, use the ".method" shortcut
Use / to call static methods
Use doto to make dealing with (mutable) classes more tolerable