- Pages: 288
- Published: May 2018
- ISBN: 9781680503005
- read May 20th 2022.
Leiningen is a project manager, REPL runner and more. To start the (better than default) REPL, run
lein repl
Clojure programmers adopted the convention to name the symbols "lower-case-with-dashes". That is referred to as kebab case
Usually, Clojure can only use symbols after they've been defined, but it's possible to declare them with
(declare say-welcome ...)
Mostly clojurists stick to defining function before using them, reserving declare
to mutually recursive fns.
Division will return a ratio, which is specific to Clojure.
;; division / will give a ratio
(/ 8 3) ;=> 8/3
;; to get int truncation, use "quot"(ient)
(quot 8 3) ;=> 2
cons
(truct) will prepend and return a sequence.
conj
(junction) will append and return a vector (if performed on a vector).
Vectors are continuous chunk of memory. Lists are implemented as linked lists. So getting 654'th element of a vector is faster than a list. For lists, it's simpler to prepend, for vectors to append. That's why conj
will prepend a list, but append a vector.
Vectors can be created with literal syntax []
or with vector
fn.
;; the same as “[true 3 "four" 5]”
(vector true 3 "four" 5)
Most useful functions for vectors (and other collections) are first
, rest
, nth
, and count
.
(def year-books ["1491" "1984" "2001" "April 1856"])
(count year-books)
;=> 4
(nth year-books 2)
;=> "2001"
;; it's also possible to call vector as a function
;; with index as an argument, so the next is eq to last call
(year-books 2)
When passing lists around, they need to be quoted, otherwise Clojure will try to evaluate them - since fn calls are lists where the first item is the fn name.
'(1 2 3)
Lists can also be created with list
function.
(list 1 2 3 "four" 5 "six")
The method to create a map is hash-map
.
Getting elements from map can be done like
(get map-name "key")
;; this is the same
(map-name "key")
Order in maps is not guaranteed, there is sorted-map
where order is important.
Keywords are labels. Using keywords as keys in maps, make it possible to use keyword as a function call to get values from maps.
Adding stuff to maps is done with assoc
(assoc book :page-count 362)
Sets have unique values. Look similar to maps
(def genres #{:scifi :mystery :thriller})
Careful with fetching values if "nil" is possible value. For example:
(book3 :published)
;=> nil
(contains? book3 :published)
;=> true
if
is not a fn, it's a special form. Because not all arguments will be evaluated.
(if condition
(then)
(else))
=
is a function. So is not=
. We also have a logical not
, or
and and
.
What I expect as conditional operators are here too, >
, <
, <=
and >=
.
There's a number of "is that a?" functions:
number?
, string?
, keyword?
, map?
, vector?
.
false
and nil
are only falsy values, everything else is truthy. 0
is also truthy! Since no vector, even empty one equals false
, every vector is truthy.
Since if
only allows a single expression, we can use do
to evaluate multiple expressions.
(do
(println "This is four expressions")
(println "All grouped together as one")
(println "That prints stuff and then evaluates to 44")
44)
For situations where we only want to have if...then
, without the else
, Clojure has when
, which has an implicit do
.
cond
is similar to a switch
. If no predicates come back as truthy, it evaluates nil
.
(defn shipping-charge [preferred-customer order-amount]
(cond
preferred-customer 0.0
(< order-amount 50.0) 5.0
(< order-amount 100.0) 10.0))
For a "default" kind of behavior, we have :else
keyword.
(defn shipping-charge [preferred-customer order-amount]
(cond
preferred-customer 0.0
(< order-amount 50.0) 5.0)
(< order-amount 100.0) 10.0)
:else (* 0.1 order-amount))
More similar to switch
is case
. The constants being checked are constant and won't get evaluated.
(defn customer-greeting [status]
(case status
:gold "Welcome welcome welcome back!!!"
:preferred "Welcome back!"
"Welcome to Blotts Books"))
(try
(publish-book book)
(catch ArithmeticException e
(println "Math problem")))
ex-info
takes a string describing the error and a (possibly empty) map with any other important info.
(defn publish-book [book]
(when (not (:title book))
(throw (ex-info "A book needs a title!" {:book book})))
;...
)
To catch the exception thrown like this, I need to look for exceptions of type clojure.lang.ExceptionInfo
.
Clojure supports functions with variable number of arguments. For example, a common pattern is a function which has one arity, usually with most arguments - which does something, and other arities, ones with fewer arguments, fill in the missing arguments and call that main version.
(defn greet
([to-whom]
(greet "Hello" to-whom))
([message to-whom]
(println message to-whom)))
Variadic functions can take any number of arguments. They often use &
symbol to break up the arguments. They are also called varargs.
(defn first-argument [& args]
(first args))
;; same!
(defn first-argument [x & args]
x)
Allow to have a single function with multiple implementations. Where multi-arity fns allow to pick the implementation based on the number of args, multimethods pick the implementation based on any characteristic of arguments.
Writing multimethods follows the recipe:
- define the dispatch function - what are we differentiating between arguments - based on what are we going to handle them differently?
- define the multimethod and the dispatch fn it uses
- write the separate methods for each dispatch case
;; 1. let's say we need to calculate royalties based on the copyright
(defn dispatch-published [book]
(cond
(< (:published book) 1928) :public-domain
(< (:published book) 1978) :old-copyright
:else :new-copyright))
(defmulti compute-royalties dispatch-published)
(defmethod compute-royalties :public-domain [book]
0)
(defmethod compute-royalties :old-copyright [book]
;; compute royalties...
)
(defmethod compute-royalties :new-copyright [book]
;; compute royalties...
)
To avoid blowing the stack, instead of calling the function name, call recur
.
(defn sum-copies-l [books]
(loop [books books total 0]
(if (empty? books)
total
(recur
(rest books)
(+ total (:copies-sold (first books))))
)))
Useful to check arguments before continuing with the function These accept a vector of expressions to evaluate. If any evaluates to false, the exception is raised.
(defn publish-book [book]
{:pre [(:title book)]}
(print-book book)
(ship-book book))
:post
condition lets you check on the value returned from the function. E.g. to ensure that returned value is Boolean:
(defn publish-book [book]
{:pre [(:title book)]
:post [(boolean? %)]}
(print-book book)
(ship-book book))
Functions are values. Can be returned from other functions, can be passed to functions.
When I want to use a function on a collection of arguments.
E.g. concat a vector of strings:
(def words ["A" " cheese " " is " " better " " than " " dirt!"])
(apply str words)
Constructs a fn with one or more of the parameters "baked in". It partially fills in the arguments, producing a new fn.
(def my-inc (partial + 1))
Wraps a function supplied with a call to not
.
(def not-number? (complement number?))
Combines predicate fns and ands them together.
(def odd-number? (every-pred odd? number?))
It's the fn shorthand notation
; a number doubler
#(* % 2)
A style suggestion is not to use numbered literals, better use full anon fn notation.
Calling def
within a fn will make a value globally visible - its a big no-no. Might be useful for debugging purposes tho :)
So assigning values to symbols ("creating variables") in fn's local scope is what let
does.
(defn compute-discount-amount
[amount discount-percent min-charge]
(let [discounted-amount (* amount (- 1.0 discount-percent))]
(if (> discounted-amount min-charge)
discounted-amount
min-charge)))
if-let
will conditionally bind values.
Suppose we have these values:
(def anon-book {:title "Sir Gawain and the Green Knight"})
(def with-author {:title "Once and Future King" :author "White"})
In the function which returns uppercase name of author, we need to check if the author key exists in the map, otherwise the code will blow up trying to uppercase nil
.
(defn uc-author [book]
(if-let [author (:author book)]
(.toUpperCase author) ; then case
"ANONYMOUS" ; else case
))
There is also a when-let
.
(defn uppercase-author [book]
(when-let [author (:author book)]
(.toUpperCase author)))
def
binds a symbol to a value.
Binding created by def exists until the program terminates or is overwritten. That makes is good for constants.
;; Everyone's favorite universal constant.
(def PI 3.14)
When you evaluate a def
, Clojure creates a var which holds the binding of a symbol to a value.
+------------+------------+
| PI | 3.14 |
+------------+------------+
(def author "Austen")
'author ; the symbol author, not it's value
#'author ; var for author
;;=>user/author
(def the-var #'author)
(.get the-var) ; get the value of the-var: "Austen"
(.-sym the-var); get the symbol for the var: author
Changing the value of vars?
(def debug-enabled false)
(defn debug [msg]
(if debug-enabled
(println msg)))
For situations like this, Clojure gives binding
. Very similar to let
. However, any var used in binding needs to be declared as dynamic. Also, the convention is that dynamic vars should start and end with *. Clojurists refer to surrounding asterisks as earmuffs. So we can at a glance see which vars are usable in binding.
(def ^:dynamic *debug-enabled* false)
(defn debug [msg]
(if *debug-enabled*
(println msg)))
(binding [*debug-enabled* true]
(debug "Calling that darn function")
(some-troublemaking-function-needing-logging)
(debug "back from nasty function"))
Keep in mind that let
does not create vars.
print-length is dynamic var used in Clojure. We can make it different with using set!
(def books "2001" "1984" "2010" "Tribe of the Cave Bear")
(set! *print-length* 2)
books
;=> ["2001" "1984"...]
*1
is the last result from REPL. *2 next to last, *3 one before. *e is the last exception in REPL.
Vars live in namespaces. Basically, a ns is a big lookup table of vars, indexed by their symbols.
ns
creates a namespace and makes it a current namespace. I can also use it to switch between namespaces.
To get a symbol from another ns, issue a fully qualified symbol, one that includes the namespace.
When working with files (.clj), we need to make sure that the file is loaded before trying to use the ns.
For example, let's use diff
fn from clojure.data
.
(def literature ["Emma" "Oliver Twist" "Posession"])
(def horror ["It" "Posession" "Carrie"])
;; this would throw an exception
(clojure.data/diff literature horror)
;; because we haven't loaded the namespaced function, we need to require it
(require 'clojure.data)
Filename has to correspond to the namespace:
blottsbooks.core
=> src/blottsbooks/core.clj
. To create a new namespace/file, let's say pricing
- within src/blottsbooks/pricing.clj
and (ns blottsbooks.pricing)
.
Note that dashes in the namespace are converted to underscores in the file system. E.g. blotts-books.current-pricing
gets converted to blotts_books/current_pricing.clj
.
To fold the require in a ns
declaration:
(ns blottsbooks.core
(:require blottsbooks.pricing)
(:gen-class))
When using require
stand-alone, it's a symbol, used as (require)
and when used in ns
it's :require
, a keyword. In standalone version the argument must be 'quoted. In ns
version it must not be quoted.
We can alias the required namespace with :as
.
:refer
pulls the vars from another namespace. They can then be used without fully qualified name. However, since it can overwrite an existing fn, it should be used sparingly.
(require '[blottsbooks.pricing :refer [discount-price]])
Current namespace is always bound to *ns*
.
It's possible to look up any existing namespace with find-ns
.
(find-ns 'user)
Now you can get everything that ns knows about with ns-map
(ns-map (find-ns 'user))
Keywords also have a room for a namespace, like :blottsbooks.pricing/author
. And if we're in the namespace, then ::author
works too. Double colon evaluates to "this namespace".
Getting to other projects written in Clojure is done via Leiningen's project file, within :dependencies
.
To wrap a collection in a sequence:
(def title-seq (seq ["Emma" "Oliver Twist" "Carrie"]))
Calling seq on a map will give a sequence of key/value pairs.
(seq {:title "Emma", :author "Austen", :published 1815})
;=>([:title "Emma"] [:author "Austen"] [:published 1815])
Calling seq
on an empty collection gives nil
.
first
gives the first element. rest
gives everything after the first elem. next
gives everything but the first element. Difference between next
and rest
is that rest
of an empty sequence is empty sequence. next
of an empty sequence is nil
.
You can also add a new element on the start of the sequence with cons
.
rest
, next
and cons
always return sequences. sort
also takes a collection, sorts it and returns a sequence. reverse
as well.
partition
takes a flat structure and chops it up in a sequence of smaller sequences.
(def book-list ["2001" "Clarke" "Foundation" "Asimov"])
(partition 2 book-list)
interleave
weaves two sequences together.
(def titles ["Večernji akt" "Smogovci"])
(def authors ["Pavličić" "Hitrec"])
(interleave titles authors)
interpose
sprinkles a separator between sequence elements.
(def scary-animals ["tigers" "bears" "lions"])
(interpose "and" scary-animals)
Like filter
, some
takes a predicate fn and a collection. It returns the first truthy value from the predicate fn or nil
if it doesn't find any.
map
takes a function and a collection.
(map #(count (:title %)) books)
We also have a for
function.
(for [b books]
(count (:title b)))
reduce
takes all the elements in the collection and combines them into one. Takes a fn and a sequence, calls the fn for each element of the sequence. Passes two arguments to the fn. Element of the collection and current result.
line-seq
turns contents of a file in a sequence
->>
will put the result as the last argument, while ->
puts the result as the first argument.
Suppose we want a bunch of nonsense text, for testing purposes...
(def jack "All work and no play makes Jack a dull boy")
(def repeated-text (repeat jack))
repeat
will return a lazy sequence. Lazy sequence is the one which waits until asked before generating it's elements. An unbounded sequence is a lazy sequence which, in theory, could go on forever.
A bit more interesting fn is cycle
. It takes a collection and returns a sequence with the collection repeated over and over.
(take 7 (cycle [1 2 3]))
;=> (1 2 3 1 2 3 1)
iterate
can generate even more interesting sequences.
(def numbers (iterate inc 1))
(nth numbers 101)
;=> 102
^ numbers
in principle contains all positive integers.
take
is also lazy. It won't actually evaluate values until asked for them.
(def many-nums (take 1000000000 (iterate inc 1)))
(take 20 many-nums)
So is map
:
(def evens (map #(* 2 %) (iterate inc 1)))
(take 20 evens)
;=>(2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40)
lazy-seq
is similar to seq
. It constructs a seq, but values won't be evaluated until called.
(defn my-repeat [x]
(cons x (lazy-seq (my-repeat x)))
)
(defn my-iterate [f x]
(cons x (lazy-seq (my-iterate f (f x)))))
Let's say we want to get 10 chapters from files
(def chapters (take 10
(map slurp
(map #(str "chap" % ".txt") numbers))))
However, this sets up a pipeline for reading, but hasn't read anything from the disk yet. For these situations, there's a doall
function.
(doall chapters)
doseq
is similar to for, to evaluate all the elements of the lazy sequence, but doesn't hold on to the evaluated thing.
(doseq [c chapters]
(println "The chapter text: " c))
Be careful, calling count
, sort
or reduce
over an infinite sequence is a bad idea.
Let's pry open some data:
(def authors [:monet :austen])
;; destructure in let
(let [[painter novelist] authors]
(println "Painter is " painter)
(println "Novelist is " novelist))
What if there's more elements and we want to skip some? The convention is to use the underscore.
(def artists [:monet :austen :beethoven :dickinson])
(let [[_ _ composer poet] artists]
(println "The composer is " composer)
(println "The poet is " poet))
Destructuring also works in function arguments. You just don't supply the value, it comes from the function call.
(defn artist-description [[novelist poet]]
(str "The novelist is " novelist " and the poet is " poet))
; call
(artist-description [:austen :dickinson])
Maps can also be destructured.
(def artist-map {
:painter :monet
:novelist :austen
})
(let [{painter :painter writer :novelist} artist-map]
(println painter "is a painter")
(println writer "is a novelist"))
Multilevel map destructuring
(def sith {
:name "Darth Vader"
:parents {:mother "Shmi" :father "The Force"}
})
(let [{{dad :father mom :mother} :parents} sith]
(println "Vader's dad is" dad)
(println "Vader's mom is" mom))
Mixing and matching
(def authors [
{:name "Jane Austen" :born 1775}
{:name "Charles Dickens" :born 1812}
])
(let [[{dob-1 :born} {dob-2 :born}] authors]
(println "One author was born in" dob-1)
(println "Other author was born in" dob-2))
We can pull out keyword keys and assign symbols of same name.
(def character {:name "Romeo" :age 16 :gender "male"})
; if we want to bind :name to name, :age to age and :gender to gender, you can pull out :keys
(defn character-desc [{:keys [name age gender]}]
(str "Name:" name ", age:" age ", gender: " gender))
When destructuring fn arguments, it's still possible to get the entire map which was destructured. The following funtions are equivalent
(defn add-greeting [character]
(let [{:keys [name age]} character]
(assoc character
:greeting
(str "Hello, my name is " name " and I'm " age " years old.")
)))
;; same, simpler, using :as
(defn add-greeting[{:keys [name age] :as character}]
(assoc character
:greeting
(str "Hello, my name is " name " and I'm " age " years old.")))
(defn mysql
"Create a database specification for a
mysql database. Opts should include
keys for :db, :user, and :password.
You can also optionally set host and port.
Delimiters are automatically set to \"`\"."
[{:keys [host port db make-pool?]
:or {host "localhost", port 3306, db "", make-pool? true}
:as opts}]
;; Do something with host, port, db, make-pool? and opts
)
Records are like maps with predefined keys. Maps can incur a resource penalty in more intensive use cases.
(defrecord FictionalCharacter [name appears-in author])
(def watson (->FictionalCharacter "John Watson" "Sign of the Four" "Doyle"))
Once created, I treat records just like a map (:appears-in watson)
. Any fn which works with a map, will work with a record.
(count elizabeth)
(keys watson)
Getting keys from a record is faster than from a map.
Even though it's possible to inspect the type of the record with class
and instance?
, these should not be used in the code. For those type-sensitive situations we have protocols
Enable to treat different records in a unified way. They are polymorphic and behave depending on the type of their first argument.
Example from the Component library:
(defprotocol Lifecycle
(start [component]
"Begins operation of this component. Synchronous, does not return
until the component is started. Returns an updated version of this
component.")
(stop [component]
"Ceases operation of this component. Synchronous, does not return
until the component is stopped. Returns an updated version of this
component."))
In cases I want to test the protocol, I can create a one-off implementation. To do that, Clojure provides a function reify
. Not all methods from the protocol need to be implemented.
(def test-component (reify Lifecycle
(start [this]
(println "START") this)
(stop [this]
(println "STOP") this)))
Clojure comes with the library for writing unit tests - clojure.test
. In a Clojure project the tests usually live in the test
subdir. The convention is to put tests for a namespace in a parallel -test
module.
(deftest test-finding-books
(is (not (nil? (i/find-by-title "Emma" books))))
(is (nil? (i/find-by-title "XSDD" books))))
It's convenient to organize tests into subtests/contexts with testing
.
(deftest test-basic-inventory
(testing "Finding books")
(is (not (nil? (i/find-by-title "Emma" books))))
(is (nil? (i/find-by-title "XSDD" books)))
(testing "Copies in inventory"
(is (= 10 (i/number-of-copies-of "Emma" books))))
)
Ways to run tests in a namespace:
(test/run-tests)
(test/run-tests *ns*)
(test/run-tests 'inventory.core-test)
Or, even better, run tests from the command line with lein test
This enables us to state the property we want to test, along with the description of the input data for which that property should hold.
See also inventory example tests.
Clojure library which enables us to do this is test.check. It provides a number of generators to create more or less random data for testing.
(def title-gen gen/string-alphanumeric)
;; generates alnum strings
(def copies-gen gen/pos-int)
;; generates positive integers
Careful! These generators can also generate empty strings or zeros. To use only viable values, we can use such-that
from the library.
(def title-gen
(gen/such-that not-empty gen/string-alphanumeric))
(def copies-gen
(gen/such-that (complement zero?) gen/pos-int))
The test.check implementation of hash-map
allow us to pass the generators and end up with an endless supply of maps with the keys associated with values from the generators.
(def book-gen
(gen/hash-map :title title-gen :author author-gen :copies copies-gen))
;; now we can generate an endless supply of inventories
(def inventory-gen
(gen/not-empty (gen/vector book-gen)))
We also need a book, as an element from an inventory along with the inventor which contains it.
(def inventory-and-book-gen
(gen/let [inventory inventory-gen
book (gen/elements inventory)]
{:inventory inventory :book book}))
Final part is expressing the property.
;; given
;; (:require [clojure.test.check.properties :as prop])
;; (:require [clojure.test.check.generators :as gen])
;; this will check that each generated int is smaller than next positive int
(prop/for-all [i gen/pos-int]
(< i (inc i)))
Well, but we need to limit the test to a certain number of tests.
;; given
;; (:require [clojure.test.check :as tc])
(tc/quick-check 50
(prop/for-all [i gen/pos-int]
(< i (inc i))))
The check for the books is:
(tc/quick-check 50
(prop/for-all [i-and-b inventory-and-book-gen]
(= (i/find-by-title (-> i-and-b :book :title) (:inventory i-and-b))
(:book i-and-b))))
There is also clojure.test integration in the form of defspec
, from clojure.test.check.clojure-test
namespace.
(ctest/defspec find-by-title-finds-books 50
(prop/for-all [i-and-b inventory-and-book-gen]
(= (i/find-by-title (-> i-and-b :book :title) (:inventory i-and-b))
(:book i-and-b))))
This gives us clojure.test
test which runs the property test.
https://clojure.org/about/spec
Relatively new addition is clojure.spec
which enables us to validate the shape of values - to ensure the proper data types.
(ns inventory.core
(:require [clojure.spec.alpha :as s]))
;; key fn supplied by spec is "valid?"
(s/valid? number? 44) ;true
(s/valid? number? :hello) ;false
Using spec/and
we can combine several checks, for example, to see if the value is a number and greater than 10
(def n-gt-10 (s/and number? #(> % 10)))
There is also spec/or
, but note how it's using keywords with checks, that is to supply coherent feedback when it fails.
(def n-or-s (s/or :a-number number? :a-string string?))
(s/valid? n-or-s 10)
(s/valid? n-or-s "ha")
We can also spec collection to see if they are of certain shape.
(def coll-of-strings (s/coll-of string?))
;; or a collection of numbers or strings
(def coll-of-n-or-s (s/coll-of n-or-s))
cat
helps us specify this should follow that. For example, if we wanted to match only four element collections of alternating strings and numbers:
(def s-n-s-n (s/cat :s1 string? :n1 number? :s2 string? :n2 number?))
(s/valid? s-n-s-n ["Emma" 1815 "Jaws" 1974])
;; true
Using keys
fn, we can also write specs for maps.
(def book-s
(s/keys :req-un [:inventory.core/title
:inventory.core/author
:inventory.core/copies]))
This spec will match any map which has :title, :author and :copies.
Using clojure.spec/def
we can register a spec to be used globally, or JVM-wide registry of specs.
(s/def
:inventory.core/book
(s/keys
:req-un
[:inventory.core/title :inventory.core/author :inventory.core/copies]))
This registers our book under the kw :inventory.core/book, now we can use it as a spec
(s/valid? :inventory.core/book {:title "Dracula" :author "Stoker" :copies 10})
Tightening up a spec:
;; inventory.core assumed, hence ::
(s/def ::title string?)
(s/def ::author string?)
(s/def ::copies int?)
(s/def ::book (s/keys :req-un [::title ::author ::copies]))
To understand why a spec is failing, we can use explain
, which takes same arguments as valid
:
(s/explain ::book {:author :austen :title :emma})
While explain
prints what went wrong with a spec, conform
will tell about the successful match.
(s/conform s-n-s-n ["Emma" 1815 "Jaws" 1974])
Automatically check the arguments of a function, by taking advantage of pre and post conditions.
;; an inventory is a collection of books
(s/def :inventory.core/inventory (s/coll-of ::book))
(defn find-by-title
[title inventory]
{:pre [(s/valid? ::title title)
(s/valid? ::inventory inventory)]}
(some #(when (= (:title %) title) %) inventory))
However, clojure.spec
provides a fn to enable this, spec/fdef
.
;; Define the fn
(defn find-by-title
[title inventory]
(some #(when (= (:title %) title) %) inventory))
;; Register a spec
(s/fdef find-by-title
:args (s/cat :title ::title
s/cat :inventory ::inventory))
Because of the performance penalty, the checking is disabled by default. For that reason, it's the best to use it only during testing and development.
To enable argument checking, we need to require another namespace.
(require '[clojure.spec.test.alpha :as st])
And explicitly instrument our fn
(st/instrument 'inventory.core/find-by-title)
We can use fdef
to drive test.check
generative tests.
(defn book-blurb [book]
(str "The best selling book " (:title book) " by " (:author book)))
(s/fdef book-blurb :args (s/cat :book ::book))
Now all we need is check
function
(require '[clojure.spec.test.alpha :as stest])
(stest/check 'inventory.core/book-blurb)
Even though we're calling the fn with randomly generated data, we're not checking the return value.
(s/fdef book-blurb
:args (s/cat :book ::book)
:ret (s/and string? (partial re-find #"The best selling"))
)
We can also use a fn
(defn check-return [{:keys [args ret]}]
(let [author (-> args :book :author)]
(not (neg? (.indexOf ret author))))
)
(s/fdef book-blurb
:args (s/cat :book ::book)
:ret (s/and string? (partial re-find #"The best selling"))
:fn check-return
)
Java class method for getting a file:
(def authors (java.io.File. "authors.txt"))
This creates a new instance of java.io.File
, calling the constructor, and returns that instance. Now we can call methods on that instance. To check if the file exists:
(if (.exists authors)
(println "There is a file authors.txt")
(println "No such file :'("))
(if (.canRead authors)
(println "we can read it!"))
;; if we have appropriate permissions
(.setReadable authors true)
Accessing public fields of Java objects
(def rect (java.awt.Rectangle. 0 0 10 20))
(println "Width " (.-width rect))
(println "Height: " (.-height rect))
To avoid using fully qualified Java package names, we can use import
much in the same way as require/:as
;; in the REPL
(import java.io.File)
;; a the top of the file
(ns read-authors
(:import java.io.File))
;; once imported, we can refer to the class without its package name
(def author (File. "authors.txt"))
Note that importing a single class doesn't need a quote, while importing multiple classes from the same package does require quote.
;; in clj file
(ns read-authors
(:import (java.io File InputStream)))
;; in the REPL
(import '(java.io File InputStream))
The package java.lang
is automatically imported.
Java classes themselves are objects of instance java.lang.Class
with fields and methods of their own. These static fields and methods are independent of any particular instance.
(import java.io.File)
File/separator
;=> "/"
Accessing static methods uses same syntax:
;; create a temporary file in the standard temp dir
(def temp-authors-file (File/createTempFile "authors_list" ".txt"))
Keep in mind that .method
and class/staticMethod
even if looking like fns, aren't. So you can't bind them:
;; nope
(def count-method .count)
It is possible to use built in memfn
to turn method name into a function. Eg. .exists
is not a fn, but (memfn exists)
is.
E.g. if we want to check a collection of files if they exist:
(def files [(File. "authors.txt") (File. "titles.txt")])
(map (memfn exists) files)
The default thread we get by simply running a program is called main thread
(ns blottsbooks.threads)
(defn -main []
(println "Coming live from the main thread"))
;; make a thread
(defn do-something-in-a-thread []
(println "Hello from the thread")
(println "Good bye from the thread"))
(def the-thread (Thread. do-something-in-a-thread))
;; and run it
(.start the-thread)
Note that Java Thread class takes any object which implements the Runnable interface. Clojure fns implement that interface, so we can pass any Clojure fn.
(defn do-something-else []
(println "Hello from a new thread")
(Thread/sleep 3000)
(println "Good bye from this new thread!"))
(.start (Thread. do-something-else))
With threads, it's not possible to know which one will finish first, last or in any order - we simply don't know the speed of their execution. Threads are independent engines of execution. Beware of the race conditions.
How do we get a value from a thread? How do we know it's done?
We can use join
which will pause and return only when the thread passed to it is finished. However, it always returns nil
.
(def del-thread (Thread. #(.delete (java.io.File. "temp-titles.txt"))))
(.start del-thread)
(.join del-thread)
promise
is like a value trap, once it catches a value, it snaps shut.
(def the-result (promise))
;; putting the value in the promise
(deliver the-result "Emma")
;; get the value out
(deref the-result)
;; or
@the-result
If you try to deref or @
a promise with no value, deref will pause until there is a value. We can run calculations in separate threads and communicate results back to our original, default thread.
;; given a book inventory map ...
(defn sum-copies-sold [inventory]
(apply + (map :sold inventory)))
(defn sum-revenue [inventory]
(apply + (map :revenue inventory)))
(let [copies-promise (promise)
revenue-promise (promise)]
(.start (Thread. #(deliver copies-promise
(sum-copies-sold inventory))))
(.start (Thread. #(deliver revenue-promise
(sum-revenue inventory)))))
;; at some later time we can use values
;; @copies-promise and @revenue-promise
Since the pattern of firing off a computation in another thread and getting a result back in a promise is so common, Clojure provides a prepackaged version - a future
, basically a promise with it's own thread.
(def revenue-future
(future (apply + (map :revenue inventory))))
;; the value is also to be got with @deref
(import java.util.concurrent.Executors)
;; a pool with at most three threads
(def fixed-pool (Executors/newFixedThreadPool 3))
(.execute fixed-pool fn-1)
(.execute fixed-pool fn-2)
(.execute fixed-pool fn-3)
;; ...
The pool will queue work and perform it as the pool frees up.
The dereferencing of a promise/future might take forever, so in real world it's wise to use a timeout.
;; wait half a second, if times out, return :oh-snap
(deref revenue-promise 500 :oh-snap)
Also, be aware that JVM will refuse to stop if there are running threads.
Since Clojure philosophy is to avoid mutable state, but state is needed, there are certain ways of dealing with state.
When we need to hold a changing state:
(def counter (atom 0))
(defn greeting-message [req]
(swap! counter inc)
(if (= @counter 500)
(str "Congrats! You are the " @counter " visitor")
(str "Welcome to Blotts Books!")))
swap!
function takes the atom to change and a function to produce the next value of the atom. swap!
also passes any additional arguments to the fn, so this also works:
(swap! counter + 12)
swap!
is also thread-safe.
Atoms can be wrapped around any Clojure value, and serve as containers for mutable values.
(ns inventory)
(def by-title (atom {}))
(defn add-book [{title :title :as book}]
(swap! by-title #(assoc % title book)))
(defn del-book [title]
(swap! by-title #(dissoc % title)))
(defn find-book [title]
(get @by-title title))
When we want to update multiple values at the same time, we can use refs
, which update values in transaction-like way.
(ns inventory)
(def by-title (ref {}))
(def total-copies (ref 0))
(defn add-book [{title :title :as book}]
(dosync
(alter by-title #(assoc % title book))
(alter total-copies + (:copies book))))
alter
behaves like swap!
, but the changes must happen within a dosync
block. And these changes either all happen or none do.
Since swap!
can be called multiple times (e.g. if different threads change it), it's a bad place to cause side effects (sending notifications, for example). Same goes for alter
and refs. For cases where we want side effects to happen in a consistent manner (where they would happen multiple times for swap!
or alter
), we use agents
.
Agents use "update via function" pattern seen with refs and atoms. The update however happens async off in another thread.
(ns inventory)
(def by-title (agent {}))
(defn add-book [{title :title :as book}]
(send
by-title
(fn [by-title-map]
;; side-effect causing fn
(notify-inventory-change :add book)
;; update to agent via fn
(assoc by-title-map title book))))
If the update to agent fails, it will raise an exception. We can check state of an agent with agent-error
and restart-agent
.
;; create an agent
(def title-agent (agent "Pride and Prejudice"))
;; update value
(send title-agent #(str % " and Zombies"))
(if (agent-error title-agent)
(restart-agent
title-agent
"Poseidon Adventure"
:clear-actions true))
Since agents rely on behind-the-scenes thread pool manageed by Clojure, and JVM can be finicky about terminating when there are live threads around - we can use shutdown-agents1
to shutdown agents in the thread pool.
It's always good idea to call shutdown-agents
just before the end of the program that uses agents.
(defn -main []
;; doing stuff with agents
(shutdown-agents)
)
Between vars and atoms and refs and agents?
- If the value is stable, with perhaps thread-wide variations ->
var
- If there's multiple values which need to be updated together, but don't involve side-effects ->
refs
- If there are side-effects needing to happen when updating mutable state or the update fn is slow ->
agent
- When we have mutable value, free of side-effects and no need to keep several values consistent ->
atom
Clojure uses the same syntax to represent code and data. read
turns characters into data structures and then eval
turns those into action.
Let's read a file full of Clojure code:
(ns codetool.core
(:require [clojure.java.io :as io]))
(defn read-source [path]
(with-open [r (java.io.PushbackReader. (io/reader path))]
(loop [result []]
(let [expr (read r false :eof)]
(if (= expr :eof)
result
(recur (conj result expr))))))
)
This is how simple it is to write a toy REPL:
(defn russ-repl []
(loop []
(println (eval (read)))
(recur)))
Strings, keywords and numbers are simple since they evaluate to themselves. Other types can be delegated to their own functions.
(defn reval [expr]
(cond
(string? expr) expr
(keyword? expr) expr
(number? expr) expr
(symbol? expr) (eval-symbol expr)
(vector? expr) (eval-vector expr)
(list? expr) (eval-list expr)
:else :completely-confused
))
;; evaluate symbols by looking them up in current namespace
(defn eval-symbol [expr]
(.get (ns-resolve *ns* expr)))
;; for vectors recursively evaluate the contents
(defn eval-vector [expr]
(vec (map reval expr)))
;; for lists evaluate all the contents like with vectors and then call apply with first element as the function name
(defn eval-list [expr]
(let [evaled-items (map reval expr)
f (first evaled-items)
args (rest evaled-items)]
(apply f args)))
Check out MAL (Make A Lisp) project.
To illustrat need for and use of macros, let's suppose we have a rating system which has three ratings - positive, neutral and negative, reprezented by a number which can be less than, equal to or greater than zero.
To avoid repeating checks for tratings, we could use a reusable construct. Similar construct was in Fortran, and names arithmetic-if
.
A naive approach would be
(defn arithmetic-if [n pos zero neg]
(cond
(pos? n) pos
(zero? n) zero
(neg? n) neg))
;; this would indeed return :meh
(arithmetic-if 0 :great :meh :boring)
;; however, this would evaluate each println:
(defn print-rating [rating]
(arithmetic-if rating
(println "Good book!")
(println "Totally indifferent.")
(println "Run away!")))
The macro which does this
(defmacro arithmetic-if [n pos zero neg]
(list 'cond (list 'pos? n) pos
(list 'zero? n) zero
:else neg))
So now we can know that this
(arithmetic-if rating :loved-it :meh :hated-it)
Will get turned into this
(cond
(pos? rating) :loved-it
(zero? n) :meh
:else :hated-it)
To make writing code macros easier Clojure gives us the code templating system of syntax quoting. To set off the syntax quoted expressions, use backtick `.
Syntax quoting uses tilde character ~ to mark the places where values should get inserted.
;; set up some values
(def n 100)
(def pos "It's positive!")
(def zero "It's a zero!")
(def neg "It's negative!")
;; plug them in cond
`(cond
(pos? ~n) ~pos
(zero? ~n) ~zero
:else ~neg)
;; run this and we get
(cond
(pos? 100) "It's positive!"
(zero? 100) "It's a zero!"
:else "It's negative!")
;; or actually, to avoid ns ambiguity
(clojure.core/cond
(clojure.core/pos? 100) "It's positive!"
(clojure.core/zero? 100) "It's a zero!"
:else "It's negative!")
Compare the syntax quoted version with original macro:
(defmacro arithmetic-if [n pos zero neg]
`(cond
(pos? ~n) ~pos
(zero? ~n) ~zero
:else ~neg))
;; without syntax quoting
(defmacro arithmetic-if [n pos zero neg]
(list 'cond (list 'pos? n) pos
(list 'zero? n) zero
:else neg))
Rembember that macros get themselves felt two times - the first time when they generate the code, and the second time when the code they have generated runs.
When you run into trouble with macros, use the macroexpand-11
fn. For example:
(macroexpand-1 '(arithmetic-if 100 :pos :zero :neg))
;; outputs
(cond (pos? 100) :pos (zero? 100) :zero :else :neg)