Clojure
Table of Contents
Elementary
conj
Add an element to this data structure.
do
do
is a way of calling multiple functions successively. Here, I'll show you:
(print (let [cnt 1] (do (println (+ cnt 1)) (+ cnt 1) (println (+ cnt 2)) (+ cnt 3))))
2 3 4
Notice how the second line within the do
does seemingly nothing, but it
does indeed print
the very last expression (+ cnt 3)
!
This is because the do
evaluates each expression inside and returns
the result of the last expression.
The definition of do
from the Clojure doc is pretty understandable:
Evaluates the expression in order and returns the value of the last. If no expressions are supplied, returns nil
.
meta symbol ^
^
is "the meta character". It tells the reader to add the symbol
starting with ^
as metadata to the next symbol (provided it is
something that implements IMetas
).
(def x ^:IamMeta [1 2 3]) x ;; => [1 2 3] (meta x) ;; => {:tag :IamMeta}
exclamation mark !
Community Clojure Style Guide recommends that the names of functions/macros that are not safe in STM (Software Transactional Memory) transactions should end with an exclamation mark, !.
Basically, end with ! functions that change state for atoms, metadata, vars, transients, agents and io as well.
Looping
while
loop
Loops in Clojure
are wierd, but it's apparently the way it is as to allow for tail-optimization.
If you have a return value, then the way to do it is the following:
(loop [cnt 10] (print (str cnt " ")) (if (zero? cnt) cnt (recur (dec cnt))))
10 9 8 7 6 5 4 3 2 1 0
As you can see in the example above, the loop keeps going until we tell it to
return some value. recur
is the in the else
clause of our if-statement
and will thus decrement the value of cnt
on every iteration until the
if-clause
is true
. Summarized:
- Start loop by calling
loop
on a vector with the initial values (can be multiple initial variables),loop [name1 value1, name2 value2 ... ]
. - Conditionally call
recur
for as long as you want to iterate - Return a value to end the loop.
You can also have a loop without returning any value. It's done in the following way:
(loop [cnt 10] (when (pos? cnt) (print (str cnt " ")) (recur (dec cnt))))
The difference here is the use of the when
macro. In fact, let's expand it!
(println (macroexpand '(when (pos? cnt) (println cnt) (recur (dec cnt)))))
(if (pos? cnt) (do (println cnt) (recur (dec cnt))))
As we can see it actually expands to a if...do...
.
See section about do for more info on, well, do
.
Abstractions and Polymorphism
Multimethods
Multimethods are basically overriding functions, but with one function defined explicitly to figure out which of the methods to use, known as a dispatch function.
(ns were-creatures) (defmulti full-moon-behavior (fn [were-creature] (:were-type were-creature))) (defmethod full-moon-behavior :wolf [were-creature] (str (:name were-creature) " will howl and murder")) (defmethod full-moon-behavior :simmons [were-creature] (str (:name were-creature) " will encourage people and sweat to the oldies")) (println (full-moon-behavior {:were-type :wolf :name "Rachel from next door"})) (println (full-moon-behavior {:were-type :simmons :name "Andy the baker"}))
Rachel from next door will howl and murder Andy the baker will encourage people and sweat to the oldies
You can also specify a default method using the dispatch value :default
.
(using '(were-creatures)) (defmethod full-moon-behavior :default [were-creature] (str (:name were-creature) " will stay up all night fantasy footballing")) (println (full-moon-behavior {:were-type :office-worker :name "Jimmy from sales"}))
Jimmy from sales will stay up all night fantasy footballing
Clojure also allows for dispatching on multiple arguments, ordering the methods by preferral, dispatching based on type, and hierarchical dispatching.
NOTE: Only one of the multimethods can have rest
(i.e. &args
) arguments.
Hierarchical dispatching
The hierarchy system supports derivation relationships between names (either symbols
or keywords
), and relationships between classes and names.
derive
function creates these relationships, and isa?
(as in "is a?") function
tests for their existence.
Note the ::
reader syntax. ::keyword
resolve a namespace.
derive
is the fundamental relatsionship-maker
(derive ::rect ::shape) (derive ::square ::rect)
parent
/ ancestors
/ descendants
and isa?
let you query the hierarchy
(parents ::rect) ; => #{:user/shape} (ancestors ::square) ; => #{:user/rect :user/shape} (descendants ::shape) ; => #{:user/rect :user/square} (isa? ::square ::shape) ; => true
Protocols
Protocols are optimized for dispatch methods according to type, but it can also
be done using multimethods. One limitation though, is that a protocol
cannot have
method signatures with rest
arguments.
I personally think protocol
looks very much like an interface
, from other languages.
Cool thing about a protocol
is that it allows you to extend any type, both in Java
and
in Clojure
!
(ns data-psychology) (defprotocol Psychodynamics "Plumb the inner depths of your data types" (thoughts [x] "The data type's innermost thoughts") (feelings-about [x] [x y] "Feelings about self or other")) (extend-type java.lang.String Psychodynamics (thoughts [x] (str x " thinks, 'Truly, the character defines the data type'")) (feelings-about ([x] (str x " is a longing for a simpler way of life")) ([x y] (str x " is envious of " y "'s simpler way of life")))) (println (thoughts "blorb")) (println (feelings-about "schmorb")) (println (feelings-about "schmorb" 2))
blorb thinks, 'Truly, the character defines the data type' schmorb is a longing for a simpler way of life schmorb is envious of 2's simpler way of life
As you can see, the defprotocol
takes the following:
- Name of the
protocol
- Optional docstring
- Method signatures have the name of the method, arguments and a optional docstring
You can also do extend-protocol
instead of extend-type
, like this:
(using '(data-psychology)) (extend-protocol Psychodynamics java.lang.String ... java.lang.Object ... )
NOTE: If you want a "default" implementation for all classes, you simply implement
the protocol
for java.lang.Object
, since everything in Java
, and thus Clojure
,
inherits from this.
Datatypes
The datatype features - deftype
, defrecord
and reify
, provide the mechanism for defining
implementations of abstractions, and in the case of reify
, instances of those implementations.
The abstractions themselves are defined either by protocols
or interfaces
.
A datatype provides a host type, (named in the case of deftype
and defrecord
, anonymous
in the case of reify
), with some structure (explicit fields in the case of deftype
and
defrecord
, implicit closure in the case of reify
), and optional in-type implementations
of abstraction methods.
Records
A record
is a custom map-like data type. You associate keys with values,
can look up values the same way as you do with a map
, and they're immutable like a map
.
The difference is that you can specify fields for a record
, and you can extend
a record
to implement a protocol
.
(ns were-records) (defrecord WereWolf [name title]) ;; Different ways to instatiate a record (WereWolf. "David" "London Tourist") (->WereWolf "Jacob" "Lead Shirt Discarder") (map->WereWolf {:name "Lucian" :title "CEO of Melodrama"})
And accessing properties on a record
is done as usual
(use '(were-records)) (def jacob (->WereWolf "Jacob" "Lead Shirt Discarder")) (println (.name jacob)) (println (:name jacob)) (println (get jacob :name))
Jacob Jacob Jacob
Any function you can use on a map
can also be used on a record
.
(use '(were-records)) (assoc jacob :title "Lead Third Wheel")
nil#were_records.WereWolf{:name "Jacob", :title "Lead Third Wheel"}
And for extending a protocol
using a record
, you can do it the usual way,
as seen above, or you can do it on the definition of the record
.
(defprotocol WereCreature (full-moon-behavior [x])) (defrecord WereWolf [name title] WereCreature (full-moon-behavior [x] (str name " will howl and murder"))) (println (full-moon-behavior (map->WereWolf {:name "Lucian" :title "CEO of Melodrama"})))
Lucian will howl and murder
Final note: accessing a record
is actually more performant that accessing a map
. Wierd, huh?
Reify
reify
defines both an anonymou type and creates an instance of that type.
The use case is where you need a one-off implementation of one or more protocols or
interfaces and would like to take advantage of local context.