0 1 2 3 4 5 6 7 8 9 10 目錄 介紹 Hello Clojure Syntax Data structure Function Abstraction Macro Concurrency Polymorphism Leiningen Jepsen lean-clojure 2 因为需要研究jepsen,首先就需要学习一下clojure这门语言,这里,笔者主要参考 如下资料进行学习: Clojure for the Brave and True Clojure From the Ground Up 这里简要的列出相关的学习笔记,希望能快速入门Clojure以及进行jepsen的使用。 lean-clojure 3介紹 Why Clojure 首先,为什么选择Clojure?第一个原因当然在于jepsen是用Clojure编写的,但除此 之外,Clojure也有其他吸引我去学习的地方。 Lisp。Clojure是一门Lisp方言,如果喜欢函数式编程的同学一下子就会非常喜 欢。虽然我没有任何函数式编程的经验,但以前因为受到《计算机程序的构造 与解释》这本书的影响,一直想找个机会好好学习一门Lisp语言。 JVM。最开始的Clojure是是运行在JVM上面的,当然现在也支持了其他平台 (譬如.net),运行在JVM上面的好处就在于跨平台了,并且能很方便的使用 java的library。笔者之前也没有任何java开发经验,正好也能对java相关的函数 了解一点。 Concurrency。函数式编程语言天生就是支持并发编程的,因为数据都是 immutability的。Clojure还提供了Software Transactional Memory, Agent等, 让并发编程更加简单。 Start Clojure 下载下来的Clojure包就是一个JAR文件,我们可以直接用java运行,在这里,笔者 使用的是Clojure 1.7.0,进入Clojure目录之后,运行: java -cp clojure-1.7.0.jar clojure.main Clojure 1.7.0 user=> (+ 1 2) 3 不过多数时候,我们都是使用lein来进行Clojure的项目构建以及REPL的执行,笔者 使用的是最新的lein版本,在mac下面,直接 brew install leiningen 即可安装,安装成功之后,运行lein repl就可以进行REPL。 lean-clojure 4Hello Clojure lein repl nREPL server started on port 56289 on host 127.0.0.1 - nrepl://127.0.0.1:56289 REPL-y 0.3.7, nREPL 0.2.10 Clojure 1.7.0 user=> 我们通过lein建立第一个Clojure工程。 lein new app clojure-noob 进入clojure-noob目录,运行lein run,我们会得到如下输出: lein run Hello, World! 我们可以在src/clojure_noob/core.clj这个文件里面进行编辑,将World换成 Clojure,如下: (defn -main "I don't do a whole lot ... yet." [& args] (println "Hello, Clojure!")) 再次运行lein run,得到: lein run Hello, Clojure! 通过lein run的方式可以很方便的执行代码,但是如果要将我们的代码share出去, 就需要生成一个jar文件了,我们使用lein uberjar来生成jar,生成的jar文件为 target/uberjar/clojure-noob-0.1.0-SNAPSHOT-standalone.jar,我们可以在java里 面直接运行了。 lean-clojure 5Hello Clojure java -jar target/uberjar/clojure-noob-0.1.0-SNAPSHOT-standalone.jar Hello, Clojure! lean-clojure 6Hello Clojure Clojure的语法非常的简单,只要熟悉Lisp,几乎可以无缝使用Clojure了。 Form Clojure的代码是由一个一个form组成的,form可以是基本的数据结构,譬如 number,string等,也可以是一个operation,对于一个operation来说,合法的结构 如下: (operator operand1 operand2 ... operandn) 第一个是operator,后面的就是该operator的参数,譬如 (+ 1 2 3) ,operator就 是“+”, 然后参数为1, 2, 3,如果我们执行这个form,得到的结果为6。 Control Flow Clojure的control flow包括if,do和when。 If If的格式如下: (if boolean-form then-form optional-else-form) 如果boolean-form为true,就执行then-form,否则执行optional-else-form,一些例 子: lean-clojure 7Syntax user=> (if false "hello" "world") "world" user=> (if true "hello" "world") "hello" user=> (if true "hello") "hello" user=> (if false "hello") nil Do 通过上面的if可以看到,我们的then或者else只有一个form,但有时候,我们需要在 这个条件下面,执行多个form,这时候就要靠do了。 user=> (if true #_=> (do (println "true") "hello") #_=> (do (println "false") "world")) true "hello" 在上面这个例子,我们使用do来封装了多个form,如果为true,首先打印true,然 后返回“hello”这个值。 When When类似if和do的组合,但是没有else这个分支了, user=> (when true #_=> (println "true") #_=> (+ 1 2)) true 3 nil, true, false lean-clojure 8Syntax Clojure使用nil和false来表示逻辑假,而其他的所有值为逻辑真,譬如: user=> (if nil "hello" "world") "world" user=> (if "" "hello" "world") "hello" user=> (if 0 "hello" "world") "hello" user=> (if true "hello" "world") "hello" user=> (if false "hello" "world") "world" 我们可以通过 nil? 来判断一个值是不是nil,譬如: user=> (nil? nil) true user=> (nil? false) false user=> (nil? true) false 也可以通过 = 来判断两个值是否相等: user=> (= 1 1) true user=> (= 1 2) false user=> (= nil false) false user=> (= false false) true 我们也可以通过and和or来进行布尔运算,or返回第一个为true的数据,如果没有, 则返回最后一个,而and返回第一个为false的数据,如果都为true,则返回最后一 个为true的数据,譬如: lean-clojure 9Syntax user=> (or nil 1) 1 user=> (or nil false) false user=> (and nil false) nil user=> (and 1 false 2) false user=> (and 1 2) 2 def 我们可以通过def将一个变量命名,便于后续使用,譬如: user=> (def a [1 2 3]) #'user/a user=> (get a 1) 2 lean-clojure 10Syntax Clojure有如下几种基本的数据类型,它们都是immutable的,也就是说,我们不可 能更改它们,任何对原有数据结构的修改都会生成一份新的copy。 Number Clojure的number包括int,float以及ratio,譬如下面这些都是number: 1 ; int 1.0 ; double 1 / 5 ; ratio String Clojure的string是用双引号来表示的,譬如 ”abc" ,如果字符串里面有双引号,我 们使用 \" 来表示,譬如 "\"abc\"" 。 Map Clojure的map跟其他语言的一样,就是一个kv dictionary,clojure的map有hash map以及sorted map两种,不过通常我们都不用特别区分。 map使用 {} 来表示,map的key可以是keyword,也可以是基本的数据类型,譬 如 {:a 1, 2 2, 3.0 3, "4" 4} ,map里面也能嵌套map,譬如 {:a {:b 1}} 。 我们可以通过hash-map创建一个hash map,譬如 (hasp-map :a 1 :b 2) ,通 过get函数来获取map里面的数据,譬如: user=> (get {:a 1 :b 2} :a) 1 user=> (get {:a 1 :b 2} :c) nil 通过get-in获取嵌套map的数据,譬如: lean-clojure 11Data structure user=> (get-in {:a {:b 1}} [:a :b]) 1 user=> (get-in {:a {:b 1}} [:a :c]) nil user=> (get-in {:a {:b 1}} [:b :a]) nil Keyword 在map里面,我们出现了keyword的概念,keyword使用 : 来表示,通常用在map 的key上面。譬如下面这些都是keyword: :a :hello :34 :_? keyword能够被当成function,譬如: user=> (:a {:a 1 :b 2}) 1 它等价于 user=> (get {:a 1 :b 2} :a) 1 如果keyword不存在,我们也可以指定一个默认值: user=> (:c {:a 1 :b 2} "abc") "abc" user=> (get {:a 1 :b 2} :c "abc") "abc" lean-clojure 12Data structure Vector Vector就是数组,以index 0开始,使用 [] 表示。 user=> [1 2 3] [1 2 3] user=> (get [1 2 3] 0) 1 我们可以使用vector来创建一个vector,譬如: user=> (vector 1 2 3) [1 2 3] 使用conj函数往vector里面追加数据: user=> (conj [1 2 3] 4) [1 2 3 4] List List也就是链表,跟vector有一些不同,譬如不能通过get来获取元素。List使 用 () 来表示,因为 () 在Clojure里面是作为operations来进行求值的,所以我们 需要用 '() 来避免list被求值。 我们也可以使用list函数来构造list,譬如: user=> `(1 2 3) (1 2 3) user=> (list 1 2 3) (1 2 3) List不能使用get,但可以用nth函数,但需要注意out of bound的error。 lean-clojure 13Data structure user=> (nth '(1 2 3) 0) 1 user=> (nth '(1 2 3) 4) IndexOutOfBoundsException clojure.lang.RT.nthFrom (RT.java:871) 我们也能够通过conj函数在list里面追加元素,不过不同于vector,是从头插入的: user=> (conj '(1 2 3) 4) (4 1 2 3) Set Set是唯一值的集合,使用 #{} 表示,我们也可以hash-set函数来进行set的创建: user=> #{1 2 3} #{1 3 2} user=> (hash-set 1 2 3 1) #{1 3 2} 我们可以使用set函数将vector或者list转成set,譬如: user=> (set [1 2 3 1]) #{1 3 2} user=> (set '(1 2 3 1)) #{1 3 2} 我们使用conj函数在set里面添加元素: user=> (conj #{1 2} 1) #{1 2} user=> (conj #{1 2} 3) #{1 3 2} lean-clojure 14Data structure contains?用来判断某一个值是否在set里面,譬如: user=> (contains? #{:a :b} :a) true user=> (contains? #{:a :b} :c) false user=> (contains? #{:a :b nil} nil) true 我们也可以使用get来获取某个元素: user=> (get #{:a :b nil} :a) :a user=> (get #{:a :b nil} nil) nil 使用keyword的方式也可以: user=> (:a #{:a :b}) :a user=> (:c #{:a :b}) nil lean-clojure 15Data structure Clojure是一门函数式编程语言,function具有非常重要的地位。 Function Call 函数调用很简单,我们已经见过了很多例子: (+ 1 2 3) (* 1 2 3) 我们也可以将函数返回让外面使用: user=> ((or + -) 1 2 3) 6 如果我们调用了非法的函数,会报错: user=> (1 2 3) ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn user/eval1491 (form-init7840101683239336203.clj:1) 这里,1并不是一个合法的operator。 Function Define 一个函数,通常由几个部分组成: defn 函数名称 函数说明(可选) 参数列表,使用[]封装 函数body 一个例子: lean-clojure 16Function user=> (defn fun1 #_=> "a function example" #_=> [name] #_=> (str "hello " name)) #'user/fun1 user=> (fun1 "world") "hello world" user=> (doc fun1) ------------------------- user/fun1 ([name]) a function example nil Overloading 我们也可以进行函数重载,如下: user=> (defn func-multi #_=> ([](str "no arg")) #_=> ([name1](str "arg " name1)) #_=> ([name1 name2](str "arg " name1 " " name2))) user=> (func-multi) "no arg" user=> (func-multi "1") "arg 1" user=> (func-multi "1" "2") "arg 1 2" Variable arguments 我们使用 & 来支持可变参数 lean-clojure 17Function user=> (defn func-args #_=> [name & names] #_=> (str name " " (clojure.string/join " " names))) #'user/func-args user=> (func-args "a") "a " user=> (func-args "a" "b") "a b" user=> (func-args "a" "b" "c") "a b c" Destructuring 我们可以在函数参数里面通过特定的name来获取对应的collection的数据,譬如: user=> (defn f-vec1 [[a]] [a]) #'user/f-vec1 user=> (f-vec1 [1 2 3]) [1] user=> (defn f-vec2 [[a b]] [a b]) #'user/f-vec2 user=> (f-vec2 [1 2 3]) [1 2] 在上面的例子里面,我们的参数是一个vector,然后[a]以及[a b]表示,我们需要获 取这个vector里面的第一个以及第二个数据,并且使用变量a,b存储。 我们也可以使用map,譬如: user=> (defn f-map [{a :a b :b}] [a b]) #'user/f-map user=> (f-map {:a 1 :b 2}) [1 2] user=> (f-map {:a 1 :c 2}) [1 nil] 上面这个例子我们可以用 :keys 来简化, lean-clojure 18Function user=> (defn f-map-2 [{:keys [a b]}] [a b]) #'user/f-map-2 user=> (f-map-2 {:a 1 :b 2 :c 3}) [1 2] 我们可以通过 :as 来获取原始的map: user=> (defn f-map-3 [{:keys [a b] :as m}] [a b (:c m)]) #'user/f-map-3 user=> (f-map-3 {:a 1 :b 2 :c 3}) [1 2 3] Body Function的body里面可以包括多个form,Clojure会将最后一个form执行的值作为该 函数的返回值: user=> (defn func-body [] #_=> (+ 1 2) #_=> (+ 2 3)) #'user/func-body user=> (func-body) 5 匿名函数 我们可以通过fn来声明一个匿名函数 user=> ((fn [name] (str "hello " name)) "world") "hello world" 我们也可以通过def来给一个匿名函数设置名称: lean-clojure 19Function user=> (def a (fn [name] (str "hello " name))) #'user/a user=> (a "world") "hello world" 当然,我们更加简化匿名函数,如下: user=> #(str "hello " %) #object[user$eval1584$fn__1585 0x72445715 "user$eval1584$fn__1585@72445715"] user=> (#(str "hello " %) "world") "hello world" 我们使用 % 来表明匿名函数的参数,如果有多个参数,则使用 %1 , %2 来获 取,使用 %& 来获取可变参数。 user=> (#(str "hello " %) "world") "hello world" user=> (#(str "hello " %1 " " %2) "world" "a ha") "hello world a ha" user=> (#(str "hello " %1 " " (clojure.string/join " " %&)) "world" "ha" "ha") "hello world ha ha" lean-clojure 20Function Abstraction 在了解sequence之前,我们可以先了解下abstraction,abstraction的概念在很多语 言里面都有,譬如Go,interface就是abstraction: type IA interface { DoFunc() } type A struct {} func (a *A) DoFunc() { } 在上面这个例子中,struct A实现了DoFunc函数,我们就可以认为,A实现了IA。 Sequence Abstraction Clojure也提供了abstraction的概念,这里我们主要来了解下sequence abstraction。 在Clojure里面,如果这些core sequence function first,rest和cons能够用于某个 data structure,我们就可以认为这个data structure实现了sequence abstraction, 就能被相关的sequence function进行操作,譬如map,reduce等。 First first返回collection里面的第一个元素,譬如: lean-clojure 21Abstraction user=> (first [1 2 3]) 1 user=> (first '(1 2 3)) 1 user=> (first #{1 2 3}) 1 user=> (first {:a 1 :b 2}) [:a 1] Rest rest返回collection里面,第一个元素后面的sequence,譬如: user=> (rest [1 2 3]) (2 3) user=> (rest [1]) () user=> (rest '(1 2 3)) (2 3) user=> (rest #{1 2 3}) (3 2) user=> (rest {:a 1 :b 2}) ([:b 2]) Cons Cons则是将一个元素添加到collection的开头,譬如: lean-clojure 22Abstraction user=> (cons 1 [1 2 3]) (1 1 2 3) user=> (cons 1 '(1 2 3)) (1 1 2 3) user=> (cons 1 #{1 2 3}) (1 1 3 2) user=> (cons 1 {:a 1 :b 2}) (1 [:a 1] [:b 2]) user=> (cons {:c 3} {:a 1 :b 2}) ({:c 3} [:a 1] [:b 2]) 从上面的例子可以看出,Clojure自身的vector,list等都实现了sequence abstraction,所以他们也能够被一些sequence function调用: user=> (defn say [name] (str "hello " name)) #'user/say user=> (map say [1 2]) ("hello 1" "hello 2") user=> (map say '(1 2)) ("hello 1" "hello 2") user=> (map say #{1 2}) ("hello 1" "hello 2") user=> (map say {:a 1 :b 2}) ("hello [:a 1]" "hello [:b 2]") user=> (map #(say (second %)) {:a 1 :b 2}) ("hello 1" "hello 2") Collection Abstraction 跟sequence abstraction类似,Clojure里面的core data structure,譬如vector,list 等,都实现了collection abstraction。 Collection abstraction通常是用于处理整个data structure的,譬如: lean-clojure 23Abstraction user=> (count [1 2 3]) 3 user=> (empty? []) true user=> (every? #(< % 3) [1 2 3]) false user=> (every? #(< % 4) [1 2 3]) true into 一个重要的collection function就是into,sequence function通常会返回一个seq,而 into会将返回的seq转换成原始的data structure,譬如: user=> (into [] [1 2 3]) [1 2 3] user=> (into [] (map inc [1 2 3])) [2 3 4] lean-clojure 24Abstraction Macro是函数式编程里面很重要的一个概念,在之前,我们已经使用了Clojure里面 的一些macro,譬如when,and等,我们可以通过macroexpand获知: user=> (macroexpand '(when true [1 2 3]))) (if true (do [1 2 3])) user=> (doc when) ------------------------- clojure.core/when ([test & body]) Macro Evaluates test. If logical true, evaluates body in an implicit do. nil 可以看到,when其实就是if + do的封装,很类似C语言里面的macro。 defmacro 我们可以通过defmacro来定义macro: user=> (defmacro my-plus #_=> "Another plus for a + b" #_=> [args] #_=> (list (second args) (first args) (last args))) #'user/my-plus user=> (my-plus (1 + 1)) 2 user=> (macroexpand '(my-plus (1 + 1))) (+ 1 1) macro的定义比较类似函数的定义,我们需要定义一个macro name,譬如上面的 my-plus,一个可选择的macro document,一个参数列表以及macro body。body通 常会返回一个list用于后续被Clojure进行执行。 我们可以在macro body里面使用任何function,macro以及special form,然后使用 macro的时候就跟函数调用一样。但是跟函数不一样的地方在于函数在调用的时 候,参数都是先被evaluated,然后才被传入函数里面的,但是对于macro来说,参 数是直接传入macro,而没有预先被evaluated。 lean-clojure 25Macro 我们也能在macro里面使用argument destructuring技术,进行参数绑定: user=> (defmacro my-plus2 #_=> [[op1 op op2]] #_=> (list op op1 op2)) #'user/my-plus2 user=> (my-plus2 (1 + 1)) Symbol and Value 编写macro的时候,我们其实就是构建list供Clojure去evaluate,所以在macro里 面,我们需要quote expression,这样才能给Clojure返回一个没有evaluated的list, 而不是在macro里面就自己evaluate了。也就是说,我们需要明确了解symbol和 value的区别。 譬如,现在我们要实现这样一个功能,一个macro,接受一个expression,打印并 且输出它的值,可能看起来像这样: user=> (let [result 1] (println result) result) 1 1 然后我们定义这个macro: user=> (defmacro my-print #_=> [expression] #_=> (list let [result expression] #_=> (list println result) #_=> result)) 我们会发现出错了,错误为"Can't take value of a macro: #'clojure.core/let",为什 么呢?在上面这个例子中,我们其实想得到的是let symbol,而不是得到let这个 symbol引用的value,这里let并不能够被evaluate。 所以为了解决这个问题,我们需要quote let,只是返回let这个symbol,然后让 Clojure外面去负责evaluate,如下: lean-clojure 26Macro user=> (defmacro my-print #_=> [expression] #_=> (list 'let ['result expression] #_=> (list 'println 'result) #_=> 'result)) #'user/my-print user=> (my-print 1) 1 1 Quote Simple Quoting 如果我们仅仅想得到一个没有evaluated的symbol,我们可以使用quote: user=> (+ 1 2) 3 user=> (quote (+ 1 2)) (+ 1 2) user=> '(+ 1 2) (+ 1 2) user=> '123 123 user=> 123 123 user=> 'hello hello user=> hello CompilerException java.lang.RuntimeException: Unable to resolve symbol: hello in this context Syntax Quoting lean-clojure 27Macro 在前面,我们通过 ' 以及quote了解了simple quoting,Clojure还提供了syntax quoting ` user=> `1 1 user=> `+ clojure.core/+ user=> '+ + 可以看到,syntax quoting会返回fully qualified symbol,所以使用syntax quoting能 够让我们避免命名冲突。 另一个syntax quoting跟simple quoting不同的地方在于,我们可以在syntax quoting 里面使用 ~ 来unquote一些form,这等于是说,我要quote这一个expression,但 是这个expression里面某一个form先evaluate,譬如: user=> `(+ 1 ~(inc 1)) (clojure.core/+ 1 2) user=> `(+ 1 (inc 1)) (clojure.core/+ 1 (clojure.core/inc 1)) 这里还需要注意一下unquote splicing: user=> `(+ ~(list 1 2 3)) (clojure.core/+ (1 2 3)) user=> `(+ ~@(list 1 2 3)) (clojure.core/+ 1 2 3) syntax quoting会让代码更加简洁,具体到前面print那个例子,我们let这些都加了 quote,代码看起来挺丑陋的,如果用syntax quoting,如下: lean-clojure 28Macro user=> (defmacro my-print2 #_=> [expression] #_=> `(let [result# ~expression] #_=> (println result#) #_=> result#)) #'user/my-print2 user=> (my-print2 1) 1 1 lean-clojure 29Macro Clojure是一门支持并发编程的语言,它提供了很多特性让我们非常的方便进行并发 程序的开发。 Future Future可以让我们在另一个线程去执行任务,它是异步执行的,所以调用了future 之后会立即返回。譬如: user=> (future (Thread/sleep 3000) (println "I'am back")) #object[clojure.core$future_call$reify__6736 0x7118d905 {:status :pending, :val nil}] user=> (println "I'am here") I'am here nil user=> I'am back 在上面的例子中,sleep会阻塞当前线程的执行,但是因为我们用了future,所以 clojure将其放到了另一个线程中,然后继续执行下面的语句。 有时候,我们使用了future之后,还需要知道future的任务执行的结果,知识后就需 要用defer来实现了。我们可以使用defer或者 @ 来获取future的result,譬如: user=> (let [result (future (println "run only once") (+ 1 1))] #_=> (println (deref result)) #_=> (println @result)) run only once 2 2 nil deref还可以支持timeout设置,如果超过了等待时间,就返回一个默认值。 user=> (deref (future (Thread/sleep 100) 0) 10 5) 5 user=> (deref (future (Thread/sleep 100) 0) 1000 5) 0 lean-clojure 30Concurrency 我们也可以使用realized?来判断一个future是否完成 user=> (realized? (future (Thread/sleep 1000))) false user=> (let [f (future)] #_=> @f #_=> (realized? f)) true Delay Delay可以让我们定义一个稍后执行的任务,并不需要现在立刻执行。 user=> (def my-delay #_=> (delay (let [msg "hello world"] #_=> (println msg) #_=> msg))) #'user/my-delay 我们可以通过@或者force来执行delay的任务 user=> @my-delay hello world "hello world" user=> (force my-delay) "hello world" Clojure会将delay的任务结果缓存,所以第二次delay的调用我们直接获取的是缓存 结果。 我们可以将delay和future一起使用,定义一个delay操作,在future完成之后,调用 delay,譬如: lean-clojure 31Concurrency user=> (let [notify (delay (println "hello world"))] #_=> (future ((Thread/sleep 1000) (force notify)))) #object[clojure.core$future_call$reify__6736 0x2de625f3 {:status :pending, :val nil}] user=> hello world Promise Promise是一个承诺,我们定义了这个promise,就预期后续会得到相应的result。 我们通过deliver来将result发送给对应的promise,如下: user=> (def my-promise (promise)) #'user/my-promise user=> (deliver my-promise (+ 1 1)) #object[clojure.core$promise$reify__6779 0x30dc687a {:status :ready, :val 2}] user=> @my-promise 2 promise也跟delay一样,会缓存deliver的result user=> (deliver my-promise (+ 1 2)) nil user=> @my-promise 2 我们也可以将promise和future一起使用 user=> (let [hello-promise (promise)] #_=> (future (println "Hello" @hello-promise)) #_=> (Thread/sleep 1000) #_=> (deliver hello-promise "world")) Hello world Atom lean-clojure 32Concurrency 在并发编程里面, a = a + 1 这条语句并不是安全的,在clojure里面,我们可以 使用atom完成一些原子操作。如果大家熟悉c语言里面的compare and swap那一套 原子操作函数,其实对Clojure的atom也不会陌生了。 我们使用atom创建一个atom,然后使用@来获取这个atom当前引用的值: user=> (def n (atom 1)) #'user/n user=> @n 1 如果我们需要更新该atom引用的值,我们需要通过一些原子操作来完成,譬如: user=> (swap! n inc) 2 user=> @n 2 user=> (reset! n 0) 0 user=> @n 0 Watch 对于一些数据的状态变化,我们可以使用watch来监控,一个watch function包括4 个参数,关注的key,需要watch的reference,譬如atom等,以及该reference之前 的state以及新的state,譬如: lean-clojure 33Concurrency user=> (defn watch-n #_=> [key watched old-state new-state] #_=> (if (> new-state 1) #_=> (println "new" new-state) #_=> (println "old" old-state))) user=> (def wn (atom 1)) #'user/wn user=> @wn 1 user=> (add-watch wn :a watch-n) #object[clojure.lang.Atom 0x4b28dcf8 {:status :ready, :val 1}] user=> (reset! wn 1) old 1 1 user=> (reset! wn 2) new 2 2 Validator 我们可以使用validator来验证某个reference状态改变是不是合法的,譬如: user=> (defn validate-n #_=> [n] #_=> (> n 1)) #'user/validate-n user=> (def vn (atom 2 :validator validate-n)) #'user/vn user=> (reset! vn 10) 10 user=> (reset! vn 0) IllegalStateException Invalid reference state clojure.lang.ARef.validate (ARef.java:33) 在上面的例子里面,我们建立了一个validator,参数n必须大于1,否则就是非法 的。 lean-clojure 34Concurrency Ref 在前面,我们知道使用atom,能够原子操作某一个reference,但是如果要操作一 批atom,就不行了,这时候我们就要使用ref。 user=> (def x (ref 0)) #'user/x user=> (def y (ref 0)) #'user/y 我们创建了两个ref对象x和y,然后在dosync里面对其原子更新 user=> (dosync #_=> (ref-set x 1) #_=> (ref-set y 2) #_=> ) 2 user=> [@x @y] [1 2] user=> (dosync #_=> (alter x inc) #_=> (alter y inc)) user=> (dosync #_=> (commute x inc) #_=> (commute y inc)) ref-set就类似于atom里面的reset!,alter就类似swap!,我们可以看到,还有一个 commute也能进行reference的更新,它类似于alter,但是稍微有一点不一样。 在一个事务开始之后,如果使用alter,那么在修改这个reference的时候,alter会去 看有没有新的修改,如果有,就会重试当前事务,而commute则没有这样的检查。 所以如果通常为了性能考量,并且我们知道程序没有并发的副作用,那就使用 commute,如果有,就老老实实使用alter了。 我们通过@来获取reference当前的值,在一个事务里面,我们通过ensure来保证获 取的值一定是最新的: lean-clojure 35Concurrency user=> (dosync #_=> (ref-set x (ensure y))) Dynamic var 通常我们通过def定义一个变量之后,最好就不要更改这个var了,但是有时候,我 们又需要在某些context里面去更新这个var的值,这时候,最好就使用dynamic var 了。 我们通过如下方式定义一个dynamic var: user=> (def ^:dynamic *my-var* "hello") #'user/*my-var* dynamic var必须用 * 包裹,在lisp里面,这个叫做earmuffs,然后我们就能够通过 binding来动态改变这个var了: user=> (binding [*my-var* "world"] *my-var*) "world" user=> (println *my-var*) hello user=> (binding [*my-var* "world"] (println *my-var*) #_=> (binding [*my-var* "clojure"] (println *my-var*)) (println *my-var*)) world clojure world 可以看到,binding只会影响当前的stack binding。 lean-clojure 36Concurrency Clojure虽然是一门函数式编程语言,当也能很容易支持类似OOP那种 polymorphism,能让我们写出更好的抽象代码。 Multimethods 使用multimethod是一种快速在代码里面引入polymorphism的方法,我们可以定义 一个dispatching function,然后指定一个dispatching value,通过它来确定调用哪 一个函数。 譬如,我们需要计算一个图形的面积,我们知道,如果是一个长方形,那么方法就 是 with * heigth ,如果是圆形,那么就是 PI * radius * radius 。 ; define a multimethod for area with :Shape keyword. (defmulti area :Shape) (defn rect [wd ht] {:Shape :Rect :wd wd :ht ht}) (defn circle [radius] {:Shape :Circle :radius radius}) (defmethod area :Rect [r] (* (:wd r) (:ht r))) (defmethod area :Circle [c] (* (. Math PI) (* (:radius c) (:radius c)))) (defmethod area :default [x] :oops) (def r (rect 4 13)) (def c (circle 12)) 我们在repl里面执行: user=> (area r) 52 user=> (area c) 452.3893421169302 user=> (area {}) :oops Protocol lean-clojure 37Polymorphism 从上面multimethod的实现可以,multimethod只是一个polymorphic操作,如果我们 想实现多个,那么multimethod就不能满足了,这时候,我们就可以使用protocol。 protocol其实更类似其他语言里面interface,我们定义一个protocol,然后用不同的 类型去特化实现,我们以jepsen的代码为例,因为jepsen可以测试很多db,所以它 定义了一个db的protocol。 (defprotocol DB (setup! [db test node] "Set up the database on this particular node." (teardown! [db test node] "Tear down the database on this particular node." 上面的代码定义了一个DB的protocol,然后有两个函数接口,用来setup和 teardown对应的db,所以我们只需要在自己的db上面实现这两个函数就能让jepsen 调用了,伪代码如下: (def my-db (reify DB (setup! [db test node] "hello db") (teardown! [db test node] "goodbye db"))) 然后就能直接使用DB protocol了。 user=> (setup! my-db :test :node) "hello db" user=> (teardown! my-db :test :node) "goodbye db" Record 有些时候,我们还想在clojure中实现OOP语言中class的效果,用record就能很方便 的实现。record类似于map,这点就有点类似于C++ class中的field,然后还能实现 特定的protocol,这就类似于C++ class的member function了。 record的定义很简单,我们使用defrecord来定义: lean-clojure 38Polymorphism user=> (defrecord person [name age]) user.person 这里,我们定义了一个person的record,它含有name和age两个字段,然后我们可 以通过下面的方法来具体创建一个person: ; 使用类似java的 . 操作符创建 user=> (person. "siddon" 30) #user.person{:name "siddon", :age 30} ; 通过 ->person 函数创建 user=> (->person "siddon" 30) #user.person{:name "siddon", :age 30} ; 通过 map->persion 函数创建,参数是map user=> (map->person {:name "siddontang" :age 30) 因为record其实可以认为是一个map,所以很多map的操作,我们也同样可以用于 record上面。 user=> (def siddon (->person "siddon" 30)) #'user/siddon user=> (assoc siddon :name "tang") #user.person{:name "tang", :age 30} user=> (dissoc siddon :name) {:age 30} record可以实现特定的protocol,譬如: (defprotocol SayP (say [this])) (defrecord person [name age] SayP (say [this] (str "hello " name))) lean-clojure 39Polymorphism 上面我们定义了SayP这个protocol,并且让person这个record实现了相关的函数, 然后我们就可以直接使用了。 user=> (say (->person "siddon" 30)) "hello siddon" lean-clojure 40Polymorphism 在clojure,很多都是使用leiningen来进行工程的构建,包括jepsen,自然我也会使 用leiningen了。因为leiningen的命令行是lein,所以后续我们都会以lein简写。 在mac下面,可以直接使用 brew install leiningen 来安装lein,当然lein官方 也提供了其他平台的安装方式,这里不再累述。 首先,我们建立一个工程, lein new clojure-stuff ,进入clojure-stuff目录, 我们可以看到项目的目录结构: ➜ clojure-stuff git:(master) ✗ find . . ./.gitignore ./.hgignore ./CHANGELOG.md ./doc ./doc/intro.md ./LICENSE ./project.clj ./README.md ./resources ./src ./src/clojure_stuff ./src/clojure_stuff/core.clj ./test ./test/clojure_stuff ./test/clojure_stuff/core_test.clj 首先我们关注的就是project.clj文件,我们在这个文件里面定义整个项目的一些基本 属性: (defproject clojure-stuff "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.7.0"]]) lean-clojure 41Leiningen "0.1.0-SNAPSHOT"是该项目现在的版本情况,在clojure里面,如果一个项目版本 以“-SNAPSHOT”结尾,通常表明改项目还处于开发阶段,还没有正式release。 description是该项目的简要描述,url是可选的网址,license则是该项目使用的 License。这里我们重点关注一下dependencies,如果我们的项目需要依赖其他的 工程,就需要在dependencies里面设置,当然,clojure的项目一定会依赖一个 clojure版本的,这里我们使用的是1.7.0。 src目录下面就是我们项目文件了,我们更改clojure_stuff/core.clj文件: (ns clojure-stuff.core) (defn my-plus "I don't do a whole lot." [a b] (+ a b)) (ns clojure-stuff.core) 声明了一个namespace,然后我们定义了一个my- plus函数,简单进行两个数相加,通常,如果我们定义了一个函数,最好在test里 面进行测试,所以我们在test/clojure_stuff/core_test.clj里面编写如下代码: (ns clojure-stuff.core-test (:require [clojure.test :refer :all] [clojure-stuff.core :refer :all])) (deftest my-plus-test (testing "Test my plus." (is (= (my-plus 1 1) 2)))) 然后执行 lein test ,输出: ➜ clojure-stuff git:(master) ✗ lein test lein test clojure-stuff.core-test Ran 1 tests containing 1 assertions. 0 failures, 0 errors. lean-clojure 42Leiningen 我们可以改动test,让其报错: (deftest my-plus-test (testing "Test my plus." (is (= (my-plus 1 2) 2)))) 再次运行 lein test ,我们会得到错误的信息: FAIL in (my-plus-test) (core_test.clj:7) Test my plus. expected: (= (my-plus 1 2) 2) actual: (not (= 3 2)) Ran 1 tests containing 1 assertions. 1 failures, 0 errors. 如果项目需要能够直接运行,我们需要编写main函数,在src/clojure_stuff/core.clj 里面,我们编写: (defn -main [& args] (println "Hello Clojure")) 同时在project.clj里面设置: :main ^:skip-aot clojure-stuff.core 然后执行 lein run ,得到: ➜ clojure-stuff git:(master) ✗ lein run Hello Clojure 我们可以使用 lein unberjar 将项目生成一个jar供其他工程使用: lean-clojure 43Leiningen ➜ clojure-stuff git:(master) ✗ lein uberjar Warning: The Main-Class specified does not exist within the jar. It may not be executable as expected. A gen-class directive may be missing in the namespace which contains the main method. Created $(PATH)/src/clojure-stuff/target/clojure-stuff-0.1.0-SNAPSHOT.jar Created $(PATH)/src/clojure-stuff/target/clojure-stuff-0.1.0-SNAPSHOT-standalone.jar 可以看到,用lein来进行clojure的项目开发是非常方便的,这也就是为什么很多 clojure项目采用它的原因,本文仅仅是简单介绍,详细的可以参考lein的文档。 lean-clojure 44Leiningen jepsen是一个分布式测试库,我们可以使用它对某个分布式系统执行一系列操作, 并最终验证这些操作是否正确执行。 jepsen已经成功验证了很多分布式系统,我们可以在它的源码里面看到相关系统的 测试代码,包括mysql-cluster,zookeeper,elasticsearch等。 为什么要研究jepsen,主要在于我们需要进行分布式数据库tidb的测试,自己写一 套分布式测试框架难度比较大,并且还不能保证在分布式环境下面自身测试框架是 不是对的,于是使用一个现成的,经过验证的测试框架就是现阶段最好的选择了, 于是我们就发现了jepsen。 jepsen是使用clojure进行开发的,所以这也就是为什么我要学习clojure的原因,不 过比较郁闷的是,学了几天,还是没有看懂太多的代码,只能慢慢不断摸索了。 Design 一个Jepsen的测试通过会在一个control node上面运行相关的clojure程序,control node会使用ssh登陆到相关的系统node(jepsen叫做db node)进行一些测试操 作。 当我们的分布式系统启动起来之后,control node会启动很多进程,每一个进程都 能使用特定的client访问到该分布式系统。一个generator为每一个进程生成一系列 的操作,让其执行。每一个操作都会被记录到history里面。在执行操作的同时,另 一个nemesis进程会尝试去破坏这个分布式系统,譬如使用iptable断开网络连接 等。 最后,当所有操作执行完毕之后,jepsen会使用一个checker来分析验证history并且 生成相关的报表。 从上面可以看出,jepsen的设计原理其实很简单,就是对分布式系统执行一系列操 作,并且同时不停的破坏系统,最后通过验证操作的结果来检验整个分布式系统的 健壮性。 Install Jepsen的安装使用不是一件很容易的事情,因为它需要一个control node,五个db node来测试,幸运的是,我们有docker,现在docker支持了docker in docker技 术,所以我们可以很方便的使用一个docker来运行五个docker。 lean-clojure 45Jepsen Jepsen已经提供了相关的docker image,我们可以直接使用: docker run --privileged -t -i tjake/jepsen 但有时候我们需要测试自己的case,所以需要提供volumn的支持,于是我稍微修改 了一下,使用了一个定制的docker: FROM tjake/jepsen RUN mkdir /jepsen_dev VOLUME /jepsen_dev ADD ./bashrc /root/.bashrc 在启动的时候,我们可以将自己的test case mount到docker里面,便于使用,使用 docker build构建: docker build -t jepsen_dev . Example test 构建好jepsen的docker环境之后,我们就可以编写简单地测试了,参考它的文档, 我们建立一个meowdb的工程,使用jepsen 0.0.6版本,然后在meowdb_test.clj里面 写上如下代码: (ns jepsen.meowdb-test (:require [clojure.test :refer :all] [jepsen.core :refer [run!]] [jepsen.meowdb :as meowdb])) (def version "What meowdb version should we test?" "1.2.3") (deftest basic-test (is (:valid? (:results (run! (meowdb/basic-test version)))))) lean-clojure 46Jepsen 然后在meowdb.clj里面: (ns jepsen.meowdb "Tests for MeowDB" (:require [clojure.tools.logging :refer :all] [clojure.core.reducers :as r] [clojure.java.io :as io] [clojure.string :as str] [clojure.pprint :refer [pprint]] [knossos.op :as op] [jepsen [client :as client] [core :as jepsen] [db :as db] [tests :as tests] [control :as c :refer [|]] [checker :as checker] [nemesis :as nemesis] [generator :as gen] [util :refer [timeout meh]]] [jepsen.control.util :as cu] [jepsen.control.net :as cn] [jepsen.os.debian :as debian])) (defn basic-test "A simple test of MeowDB's safety." [version] tests/noop-test) 我们启动docker: docker run --privileged -t -i -v meowdb:/jepsen_dev --name jepsen jepsen_dev 然后执行 lein test ,如果没有啥意外,我们会输出如下类似的结果: lean-clojure 47Jepsen INFO jepsen.store - Wrote /jepsen_dev/meowdb/store/noop/20151211T094940.000Z/history.txt INFO jepsen.store - Wrote /jepsen_dev/meowdb/store/noop/20151211T094940.000Z/results.edn INFO jepsen.core - Everything looks good! ヽ(‘ー`)ノ {:valid? true, :linearizable-prefix [], :worlds ({:model {}, :fixed [], :pending #{}, :index 0})} Ran 1 tests containing 1 assertions. 0 failures, 0 errors. 这里仅仅是对jepsen的一个简单介绍,后续我还需要仔细研究,争取早日能用到 tidb上面。 lean-clojure 48Jepsen
还剩47页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

gmmxx

贡献于2015-12-22

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf