0%

brave clojure

Clojure is a robust, practical, and fast programming language with a set of useful features that together form a simple, coherent, and powerful tool.

开发环境

Intellij

添加插件 leinigen,cursive

Emacs

clojure-mode,cider

语法

clojure是一种 lisp方言,所以基本语法与lisp类似。

1
2
3
4
5
(+ 1 2)
=> 3

(* 1 2 3)
=> 6

数据结构

maps

maps: {}
使用 get 获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{:first-name "stanhe"}

(hash-map :a 1 :b 2)
=> {:a 1 :b 2}

(get {:a 0 :b 1} :b)
=> 1

嵌套截取
(get-in {:a 0 :b {:c "stanhe"}} [:b :c])
=> "stanhe"

({:name "stanhe"} :name)
=> "stanhe"

(:name {:name "stanhe"})
=> "stanhe"

vector

vector: ()

使用 get 获取数据 conj 添加数据到末尾

1
2
3
4
5
(vector "hello" "world" "!")
=> ["hello" "world" "!"]

(conj [1 2 3] 4) #use (doc conj) to see details
=> [1 2 3 4]

list

list: ‘()

使用 nth 获取数据 conj 添加数据到开头

1
2
3
4
5
6
7
8
9
10
'(1 2 3 4)

(nth '(7 2 1 8) 3)
=> 1

(list 1 2 3 "hello")
=> (1 2 3 "hello")

(conj '(1 2 3) 4)
=> (4 1 2 3)

nth比get慢:

因为clojure要得到list的所有索引后才开始计算nth,而get不用。

使用vector还是list?

当需要添加到seq开头的时候或者写一个宏的时候使用list,其他情况使用vector。

sets

clojure 有两种sets: hash sets and sorted sets

1
2
3
4
5
6
7
(hash-set 1 1 2 2)
=> #{1 2}

(contains? #{:a nil} nil)
=> true

使用get 获取nil总是返回nil

方法 宏 特殊表达式

特殊表达式如: if do when cond condp case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(for [x [0 1 2 3 4 5]
:let [y (* x 3)]
:when (even? y)]
y)

=> (0 6 12)

(defn test
[key]
(condp = key
:a (println "a")
:b (println "b")))

(let [v value]
(cond
(> v 90) "A"
(> v 80) "B"
(> v 70) "C"
:else "D"))

(case 1 1 "1" 2 "2")

核心方法

with (doc map) to view details

map

1
2
(map inc [1 2 3])
=> [2 3 4]

reduce

1
2
(reduce #(+ %1 (* 2 %2)) 0 [1 2 3 4])
=> 20

take

1
2
3
4
5
6
7
(take 2 [2 3 4 5])
=> (2 3)

#more:
(take 5 (repeat "ok"))

(take 15 (repeatdly #(rand-int 10)))

drop

1
2
(drop 2 [2 3 4 5])
=> (4 5)

take-while drop-while same as take and drop

some filter

1
2
(some #(> (:critter %) 3) food-journal)
(some #(and (> (:critter %) 3) %) food-journal)

others:

sort sort-by collection into conj function apply partial

apply

1
2
(apply str ["1 " "hello " "world "])
=> "1 hello world "

partial

1
2
((partial + 1) 2)
=> 3

comp

1
((comp inc *) 2 3) = (inc (* 2 3))

memoize

zipmap

1
2
(zipmap [:a :b] [1 2])
=>{:a 1 :b 2}

merge-with

1
2
(merge-with - {:lat 50 :lng 10} {:lat 15 :lng 5})
=>{:lat 45 :lng 5}

The ns macro

refer

refer filters: only,:exclude,:rename

1
2
(clojure.core/refer 'chess.taxonmy :only ['bries])
(clojure.core/refer 'chess.taxonmy :rename ['bries 'yummy])

alias

1
(clojure.core/alias 'test 'clojure.core)

require use

1
2
3
4
5
6
7
8
9
10
11
12
(require ``) # find the symb
(refer ``) # refer to use it

(require '[the-divine.svg :as svg])
# eq to this:
(require 'the-divine.svg)
(alias 'svg 'the-divine.svg)

(use 'stan.core)
# eq to this:
(require 'stan.core)
(refer 'stan.core)

ns macro

ns 宏中不用 `,一般使用 require限定引入

1
2
3
4
5
6
7
(ns handler.core
(:use [clojure.java browse io]))
# eq to this:
(in-ns 'handler.core)
(use 'clojure.java.browse)
(use 'clojure.java.io)

Macro

宏可以使用“‘” 或者syntax quoting “`”

1
2
3
4
5
(defmacro my-print
[express]
(list 'let ['result express]
(list 'println 'result)
'result))

使用syntax quoting的作用

  • 返回方法的权限定名
  • 递归quotes所有元素,可以通过~取消quote,@解开seq
1
2
3
4
5
6
7
8
9
10
11
12
13
`(+ 1 ~(inc 1))
=> (clojure.core/+ 1 2)

(defmacro code-critic
[bad good]
`(do (println "bad: " (quote ~bad)
(println "good " (quote ~good)))))

`(+ ~(list 1 2 3))
=> (clojure.core/+ (1 2 3))

`(+ ~@(list 1 2 3))
=> (clojure.core/+ 1 2 3)

当写一个宏使用 &参数的时候需要使用 @解开seq

可能存在的问题

  • 内部变量错误调用
  • 多次evaluation
    使用# auto-gensysm 生成唯一符号
1
`(let [name# "Larry Potter"] name#)

并发和并行

futures delays and promises

future

新开一个线程执行返回结果的引用,结果会被缓存,code只会执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(let [result (feture (println "this prints once")
(+ 1 1))]
(println "deref: " (deref result))
(println "@: " @result))

=> "this prints once"
=> deref: 2
=> @: 2

;;(deref (future (express) back) timeout-ms timout-value)
(deref (future (Thread/sleep 1000) 0) 10 5)
=>5

(realized? (future (thread/sleep 1000)))
=> false

(let [f (future)]
@f
(realized? f))
=> true

delays

和future 类似,也只会执行一次,且会缓存结果,使用间接引用或者force执行delay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(def gimli-headshots ["serious.jpg" "fun.jpg" "playful.jpg"])

(defn email-user
[email-address]
(println "Sending headshot notification to" email-address))

(defn upload-document
"Needs to be implemented"
[headshot]
true)

(let [notify (delay ➊(email-user "and-my-axe@gmail.com"))]
(doseq [headshot gimli-headshots]
(future (upload-document headshot)
➋(force notify))))

promises

表示一个我自己期望写入的值,使用deliverf派发,同样只能派发一次

1
2
3
4
(let [ferengi (promise)]
(future (println "Here is some Ferengi: " @ferengi)
(Thread/sleep 100)
(deliver ferengi "wisper your way")))

rolling your own quene

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(defmacro enqueue
➊ ([q concurrent-promise-name concurrent serialized]
➋ `(let [~concurrent-promise-name (promise)]
(future (deliver ~concurrent-promise-name ~concurrent))
➌ (deref ~q)
~serialized
~concurrent-promise-name))
➍ ([concurrent-promise-name concurrent serialized]
`(enqueue (future) ~concurrent-promise-name ~concurrent ~serialized)))


(time @(-> (enqueue saying (wait 200 "'Ello, gov'na!") (println @saying))
(enqueue saying (wait 400 "Pip pip!") (println @saying))
(enqueue saying (wait 100 "Cheerio!") (println @saying))))
; => 'Ello, gov'na!
; => Pip pip!
; => Cheerio!
; => "Elapsed time: 401.635 msecs"

atoms refs vars

atom,swap! ,update-in,reset!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(def fred (atom {:a 0 :b 1}))
@fred
;=> {:a 0 :b 1)

swap! receives an atom and a function as arguments. It applies the function to the atom’s current state to produce a new value, and then it updates the atom to refer to this new value. The new value is also returned. Here’s how you might increase Fred’s cuddle hunger level by one:

;; update in use get.
(swap! fred update-in [:cuddle-hunger-level] + 10)
; => {:cuddle-hunger-level 22, :percent-deteriorated 1}

(swap! fred
(fn [current-state]
(merge-with + current-state {:cuddle-hunger-level 1})))
; => {:cuddle-hunger-level 1, :percent-deteriorated 0}


This is all interesting and fun, but what happens if two separate threads call (swap! fred increase-cuddle-hunger-level 1)? Is it possible for one of the increments to get lost the way it did in the Ruby example at Listing 10-1?

The answer is no! swap! implements compare-and-set semantics, meaning it does the following internally:

It reads the current state of the atom.
It then applies the update function to that state.
Next, it checks whether the value it read in step 1 is identical to the atom’s current value.
If it is, then swap! updates the atom to refer to the result of step 2.
If it isn’t, then swap! retries, going through the process again with step 1.
This process ensures that no swaps will ever get lost.

Sometimes you’ll want to update an atom without checking its current value

(reset! fred {:cuddle-hunger-level 0
:percent-deteriorated 0})

watches and validators

Watches allow you to be super creepy and check in on your reference types’ every move. Validators allow you to be super controlling and restrict what states are allowable. Both watches and validators are plain ol’ functions.

watch

A watch is a function that takes four arguments: a key, the reference being watched, its previous state, and its new state. You can register any number of watches with a reference type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

(add-watch ref key watch-fn)
;; it's watch fn must be a fn of 4 args: a key ,the reference, its old-state,it's new-state.

(defn shuffle-speed
[zombie]
(* (:cuddle-hunger-level zombie)
(- 100 (:percent-deteriorated zombie))))

(defn shuffle-alert
[key watched old-state new-state]
(let [sph (shuffle-speed new-state)]
(if (> sph 5000)
(do
(println "Run, you fool!")
(println "The zombie's SPH is now " sph)
(println "This message brought to your courtesy of " key))
(do
(println "All's well with " key)
(println "Cuddle hunger: " (:cuddle-hunger-level new-state))
(println "Percent deteriorated: " (:percent-deteriorated new-state))
(println "SPH: " sph)))))

(reset! fred {:cuddle-hunger-level 22
:percent-deteriorated 2})
(add-watch fred :fred-shuffle-alert shuffle-alert)
(swap! fred update-in [:percent-deteriorated] + 1)
; => All's well with :fred-shuffle-alert
; => Cuddle hunger: 22
; => Percent deteriorated: 3
; => SPH: 2134

(swap! fred update-in [:cuddle-hunger-level] + 30)
; => Run, you fool!
; => The zombie's SPH is now 5044
; => This message brought to your courtesy of :fred-shuffle-alert

(remove-watch fred :fred-shuffle-alert)

Validators

1
2
3
4
5
6
7
8
9
10
11
12
(defn percent-deteriorated-validator
[{:keys [percent-deteriorated]}]
(or (and (>= percent-deteriorated 0)
(<= percent-deteriorated 100))
(throw (IllegalStateException. "That's not mathy!"))))

(def bobby
(atom
{:cuddle-hunger-level 0 :percent-deteriorated 0}
:validator percent-deteriorated-validator))
(swap! bobby update-in [:percent-deteriorated] + 200)
; This throws "Invalid reference state"

refs transaction

Refs allow you to update the state of multiple identities using transaction semantics. These transactions have three features:

  • They are atomic, meaning that all refs are updated or none of them are.
  • They are consistent, meaning that the refs always appear to have valid states. A sock will always belong to a dryer or a gnome, but never both or neither.
  • They are isolated, meaning that transactions behave as if they executed serially; if two threads are simultaneously running transactions that alter the same ref, one transaction will retry. This is similar to the compare-and-set semantics of atoms.

Clojure uses software transactional memory (STM) to implement this behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(def sock-varieties
#{"darned" "argyle" "wool" "horsehair" "mulleted" "passive-aggressive" "striped" "polka-dotted" "athletic" "business" "power" "invisible" "gollumed"})

(defn sock-count
[sock-variety count]
{:variety sock-variety
:count count})
(defn generate-sock-gnome
"Create an initial sock gnome state with no socks"
[name]
{:name name :socks #{}})
;;(def sock-gnome (ref (generate-sock-gnome "Barumpharumph")))
(def sock-gnome (ref {:name "hello" :socks #{}}))
(def dryer (ref {:name "LG 1337" :socks (set (map #(sock-count % 2) sock-varieties))}))

We’ll want to modify the sock-gnome ref to show that it has gained a sock and modify the dryer ref to show that it’s lost a sock. You modify refs using alter, and you must use alter within a transaction. dosync initiates a transaction and defines its extent; you put all transaction operations in its body. Here we use these tools to define a steal-sock function, and then call it on our two refs:

(defn steal-sock
[gnome dryer]
(dosync
(when-let [pair (some #(if (= (:count %) 2) %) (:socks @dryer))]
(let [updated-count (sock-count (:variety pair) 1)]
(alter gnome update-in [:socks] conj updated-count)
(alter dryer update-in [:socks] disj pair)
(alter dryer update-in [:socks] conj updated-count)))))
(steal-sock sock-gnome dryer)

There are a couple of details to note here: when you alter a ref, the change isn’t immediately visible outside of the current transaction. This is what lets you call alter on the dryer twice within a transaction without worry­ing about whether dryer will be read in an inconsistent state. Similarly, if you alter a ref and then deref it within the same transaction, the deref will return the new state.

1
2
3
4
5
6
7
8
9
10
11
12
(def counter (ref 0))
(future
(dosync
(alter counter inc)
(println @counter)
(Thread/sleep 500)
(alter @counter)))
(Thead/sleep 250)
(println @counter)
;=> 1 ;;in transaction thread
;=> 0 ;;in main thread
;=> 2 ;;in transaction thread

commute

commute allows you to update a ref’s state within a transaction, just like alter. However, its behavior at commit time is completely different.

Here’s how alter behaves:

1
2
3
4
1. Reach outside the transaction and read the ref’s current state.
2. Compare the current state to the state the ref started with within the transaction.
3. If the two differ, make the transaction retry.
4. Otherwise, commit the altered ref state.

commute, on the other hand, behaves like this at commit time:

1
2
3
1. Reach outside the transaction and read the ref’s current state.
2. Run the commute function again using the current state.
3. Commit the result.

vars

Dynamic Binding

First, create a dynamic var:

1
2
3
4
5
6
7
(def ^:dynamic *notification-address* "dobby@elf.org")

Notice two important details here. First, you use ^:dynamic to signal to Clojure that a var is dynamic. Second, the var’s name is enclosed by asterisks. Lispers call these earmuffs, which is adorable. Clojure requires you to enclose the names of dynamic vars in earmuffs.

(binding [*notification-address* "test@elf.org"]
*notification-address*)
; => "test@elf.org"

Dynamic Var Uses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(def ^:dynamic *troll-thought* nil)
(defn troll-riddle
[your-answer]
(let [number "man meat"]
➊ (when (thread-bound? #'*troll-thought*)
➋ (set! *troll-thought* number))
(if (= number your-answer)
"TROLL: You can cross the bridge!"
"TROLL: Time to eat you, succulent human!")))

(binding [*troll-thought* nil]
(println (troll-riddle 2))
(println "SUCCULENT HUMAN: Oooooh! The answer was" *troll-thought*))

; => TROLL: Time to eat you, succulent human!
; => SUCCULENT HUMAN: Oooooh! The answer was man meat

Altering the Var Root

Here’s how you’d create a lazy seq of random numbers between 0 and 9:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(take 5 (repeatedly (partial rand-int 10)))
; => (1 5 0 3 4)


(def alphabet-length 26)

;; Vector of chars, A-Z
(def letters (mapv (comp str char (partial + 65)) (range alphabet-length)))

(defn random-string
"Returns a random string of specified length"
[length]
(apply str (take length (repeatedly #(rand-nth letters)))))

(defn random-string-list
[list-length string-length]
(doall (take list-length (repeatedly (partial random-string string-length)))))

(def orc-names (random-string-list 3000 7000))

The dorun function does just what we need: it realizes the sequence but returns nil:

(time (dorun (map clojure.string/lower-case orc-names)))
; => "Elapsed time: 270.182 msecs"

(time (dorun (pmap clojure.string/lower-case orc-names)))
; => "Elapsed time: 147.562 msecs"

There is some overhead for creating the threads,on my test

1
2
3
4
5
tmacro.core> (time (dorun (map clojure.string/lower-case orc-names)))
"Elapsed time: 188.374934 msecs"
nil
tmacro.core> (time (dorun (pmap clojure.string/lower-case orc-names)))
"Elapsed time: 235.66113 msecs"

use partiton-all

1
2
3
(def numbers [1 2 3 4 5 6 7 8 9 10])
(partition-all 3 numbers)
; => ((1 2 3) (4 5 6) (7 8 9) (10))
1
2
3
4
5
6
7
8
9
10
11
12
(def orc-names (random-string-list 20000 300))

(time (dorun (map clojure.string/lower-case orc-names)))
(time (dorun (pmap clojure.string/lower-case orc-names)))
(time (dorun
(apply concat
(pmap (fn [name] (doall (map clojure.string/lower-case name)))
(partition-all 1000 orc-names)))))

"Elapsed time: 48.199778 msecs"
"Elapsed time: 141.007598 msecs"
"Elapsed time: 57.68705 msecs"

summary

The atom reference type allows you to create an identity that you can safely update to refer to new values using swap! and reset!. The ref reference type is handy when you want to update more than one identity using transaction semantics, and you update it with alter! and commute!.

core.async

org.clojure.core.async

1
2
3
4
5
(def echo-chan (chan))
(go (println (<! echo-chan)))
(>!! echo-chan "ketchup")
; => true
; => ketchup

buffering

1
2
3
4
5
6
7
(def echo-buffer (chan 2))
(>!! echo-buffer "ketchup")
; => true
(>!! echo-buffer "ketchup")
; => true
(>!! echo-buffer "ketchup")
; This blocks because the channel buffer is full

blockng and parking

1
2
3
4
	    Inside go block        Outside go block
put >! or >!! >!!
take <! or <!! <!!

thread

1
2
3
4
5
6
7
(thread (println (<!! echo-chan)))
(>!! echo-chan "mustard")
; => true
; => mustard

thread acts almost exactly like future: it creates a new thread and executes a process on that thread. Unlike future, instead of returning an object that you can dereference, thread returns a channel. When thread’s process stops, the process’s return value is put on the channel that thread returns:

THe Hot dog machine process you’ve been longing for

1
2
3
4
5
6
7
8
(let [c1 (chan)
c2 (chan)
c3 (chan)]
(go (>! c2 (clojure.string/upper-case (<! c1))))
(go (>! c3 (clojure.string/reverse (<! c2))))
(go (println (<! c3)))
(>!! c1 "redrum"))
; => MURDER

queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(defn append-to-file
"Write a string to the end of a file"
[filename s]
(spit filename s :append true))

(defn format-quote
"Delineate the beginning and end of a quote because it's convenient"
[quote]
(str "=== BEGIN QUOTE ===\n" quote "=== END QUOTE ===\n\n"))

(defn random-quote
"Retrieve a random quote and format it"
[]
(format-quote (slurp "http://www.braveclojure.com/random-quote")))

(defn snag-quotes
[filename num-quotes]
(let [c (chan)]
(go (while true (append-to-file filename (<! c))))
(dotimes [n num-quotes] (go (>! c (random-quote))))))

Working with JVM

Java Interop

You can call methods on an object using (.methodName object). For example, because all Clojure strings are implemented as Java strings, you can call Java methods on them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(.toUpperCase "By Bluebeard's bananas!")
; => "BY BLUEBEARD'S BANANAS!"

➊ (.indexOf "Let's synergize our bleeding edges" "y")
; => 7

Notice that Clojure’s syntax allows you to pass arguments to Java methods. In this example, at ➊ you passed the argument "y" to the indexOf method.

You can also call static methods on classes and access classes’ static fields. Observe!

➊ (java.lang.Math/abs -3)
; => 3

➋ java.lang.Math/PI
; => 3.141592653589793

import

1
2
3
4
5
(import [java.util Date Stack]
[java.net Proxy URI])
(Date.)
;=> #inst "2018-08-06T07:29:03.149-00:00"

files and input/output

spit slurp with-open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(let [file (java.io.File. "/")]
➊ (println (.exists file))
➋ (println (.canWrite file))
➌ (println (.getPath file)))
; => true
; => false
; => /

(spit "/tmp/hercules-todo-list"
"- kill dat lion brov
- chop up what nasty multi-headed snake thing")

(slurp "/tmp/hercules-todo-list")

; => "- kill dat lion brov
- chop up what nasty multi-headed snake thing"

(let [s (java.io.StringWriter.)]
(spit s "- capture cerynian hind like for real")
(.toString s))
; => "- capture cerynian hind like for real"

(let [s (java.io.StringReader. "- get erymanthian pig what with the tusks")]
(slurp s))
; => "- get erymanthian pig what with the tusks"

The with-open macro is another convenience: it implicitly closes a resource at the end of its body, ensuring that you don’t accidentally tie up resources by forgetting to manually close the resource.

(with-open [todo-list-rdr (clojure.java.io/reader "/tmp/hercules-todo-list")]
(println (first (line-seq todo-list-rdr))))
; => - kill dat lion brov

multimethods protocols

multi protocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(defmulti area (fn [shape & _] shape))
(defmethod area :triangle
[_ base height]
(/ (* base height) 2))
(defmethod area :square
[_ side]
(* side side))
(defmethod area :rectangle
[_ length width]
(* length width))
(defmethod area :circle
[_ radius]
(* Math/PI radius radius))

(defprotocol Shape
(area [this])
(perimeter [this]))
(defrecord Rectangle [width length]
Shape
(area [this] (* (:width this) (:length this)))
(perimeter [this] (+ (* 2 (:width this)) (* 2 (:length this))))
)

(defrecord Squre [side]
Shape
(area [this] (* (:side this) (:side this)))
(perimeter [this] (* 4 (:side this))))

(def sql (->Squre 4))

reify

如果希望再不必自定义类型或记录的情况下实现某个协议。

1
2
3
4
(def some-type
(reify Shape
(area [this] "I calculate area")
(perimeter [this] "I calculate perimeter")))

deftype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(defprotocol INode
(entry [_])
(left [_])
(right [_])
(contains-value? [_ _])
(insert-value [_ _]))

(deftype Node [value left-branch right-branch]
INode
(entry [_] value)
(left [_] left-branch)
(right [_] right-branch)
(contains-value? [tree v]
(cond
(= v value) true
(< v value) (contains-value? left-branch v)
(> v value) (contains-value? right-branch v)))
(insert-value [tree v]
(cond
(= v value) tree
(< v value) (Node. value (insert-value left-branch v) right-branch)
(> v value) (Node. value left-branch (insert-value right-branch v)))
)
)

(extend-protocol INode
nil
(entry [_] nil)
(left [_] nil)
(right [_] nil)
(contains-value? [_ _] false)
(insert-value [_ value] (Node. value nil nil)))