Jade Dungeon

Clojure概览

Clojure Reader

可以视为序列化工具:

(pr-str [1 2 3])          ;= "[1 2 3]"

反序列化工具:

(read-string "42")        ;= 42
(read-string "(+ 1 2)")   ;= (+ 1 2)

相对的还有prread函数,区别是这两个函数不是用字符串,而是用输入输出流 进行序列化和以序列化。

标量字面量

Nil

类似Java中的null,在逻辑上作为false

Boolean

truefalse

字符串

默认就可以跨行:

"multiline strings
are very handy"

字符

  • 反斜杠普通字符:\a\b\c……
  • Unicode编码:\u00ff
  • 八进制码:\o41

转义字符:

  • \space
  • \newline
  • \formfeed
  • \return
  • \backspace
  • \tab

关键字

关键字用冒号开头,本身就是函数。可以当用访问器来求值。

定义一个map的实例person

(def person {:name "Jade" :city "Shanghai"}
;= #'user/person'

取map中key为city的值:

(:city person)
;= "Shanghai"

双冒号表示命名空间限定:

  • 当前命名空间里的关键字:::persion
  • 限定命名空间的关键字::user/persion
  • 特定命名空间里的关键字:::alias/kw
(def pizza {:name "Ramunto's"
            :location "Claremont, NH"
            ::location "43.3734,-72.3365"})
;= #'user/pizza

pizza
;= {:name "Ramunto's", :location "Claremont, NH", 
; 	:user/location "43.3734,-72.3365"}

(:user/location pizza)         ;= "43.3734,-72.3365"
  • name函数关键字的名称。
  • namespace函数返回关键字所在的命名空间。
(name :user/location)          ;= "location"
(namespace :user/location)     ;= "user"
(namespace :location)          ;= nil

符号

可以是变量的值、Java类、本地引用等等。可以用/分隔, 表示在指定命名空间中的符号。

数字

整数

整数类型为long

常用进制:410xff040

任意进制:格式为BrNB为进制,r为值:

  • 二进制的2r1117
  • 十六进制16rFF表示255

BigInt

任意精度的整数clojure.lang.BigInt42N

BigDecimal

任意精度的浮点数java.math.BigDecimal0.001M

有理数

有理数clojure.lang.Ratio22/7

正则表达式

格式:#"regex-text",而且不用转义\

(class #"(p|h)ail")                    ;= java.util.regex.Pattern

(re-seq #"(\d+)-(\d+)" "1-3")          ;= (["1-3" "1" "3"])

(re-seq #"(...) (...)" "foo bar")      ;= (["foo bar" "foo" "bar"])

可以直接使用java.util.regex.Patternjava.util.regex中的方法, 还可以用Clojure中提供的更加方便的:re-seqre-findre-matchesclojure.string命名空间中定义的其他方法。

(def reg01 #"(p|h)ail")                ;= #'user/reg01
(re-seq reg01 "pail")                  ;= (["pail" "p"])
(re-seq reg01 "hail")                  ;= (["hail" "h"])

注释

  • ;单行注释。
  • #_是一个宏,可以让reader忽略下一个语法形式,适合注释整个语句块。
(read-string "(+ 1 2 #_(* 2 2) 8)")     ;= (+ 1 2 8)

分隔符号

Clojure里空格和逗号一样用来分隔参数与符号等内容。下面的代码是相等的:

user=> (defn silly-adder [x, y] (+, x, y))          ;= #'user/silly-adder

user=> (defn silly-adder [x  y] (+  x  y))          ;= #'user/silly-adder

用不用逗号全看程序员的喜好。

集合字面量

'(a b :name 12.5)           ;; list

['a 'b :name 12.5]          ;; vector

{:name "chas" :age 31}      ;; map

#{1 2 3}                    ;; set

命名空间

显示当前命名空间:

*ns*                    ;= #<Namespace user>

切换到(或创建并切换到)命名空间foo

(ns foo)                ;= nil
*ns*                    ;= #<Namespace foo>

使用关键字def在当前命名空间定义变量(var), 然后可以通过变量名x访问变量:

(def x 1)               ;= #'user/x

x                       ;= 1

默认已经引入了java.lang包和clojure.core中所有的var:

String                  ;= java.lang.String
Integer                 ;= java.lang.Integer
java.util.List          ;= java.util.List
java.net.Socket         ;= java.net.Socket
filter                  ;= #<core$filter clojure.core$filter@24c1b2d2>

函数调用语法

推荐优先使用语法糖,不要用直接语法:

  Java语法 Clojure语法 Clojure语法糖
操作符 !k (not k)  
类型判断 al instanceof List (instance? List ll)  
静态成员 Integer.MAX_VALUE (. Integer MAX_VALUE) (Integer/MAX_VALUE)
静态方法 Math.pow(2,10) (. Math pow 2 10) (Math/pow 2 10)
构造函数 new MyClass(100) (new MyClass 100) (MyClass. 100)
实例成员 obj1.someField (. obj1 someField) (.someField obj1)
实例方法 obj1.someMethod(obj2) (. obj1 someMethod obj2) (.someMethod obj1 obj2)

Clojure中的特殊形式

quote函数防止求值操作

quote函数用来阻止对表达式进行示值操作,Clojure reader把quote的内容转为符号, 而不是求值这个符号所指向的var。

符号:

(quote x)               ;= x

语法糖:

'x                      ;= x

quote可以嵌套:

''x                     ;= (quote x)

检查是否是符号:

(symbol? 'x)            ;= true

数据结构的符号返回的是数据结构本身:

'(+ x x)                ;= (+ x x)
;; 等于:
(list '+ 'x 'x)         ;= (+ x x)

类型是列表,不是符号:

(list?   '(+ x x))      ;= true
(symbol? '(+ x x))      ;= false

do代码块

(do
	(println "hi")
	(apply * [4 5 6]))

fnletlooptrydefn和它们的变种都隐式包含了do

def var

在「当前」命名空间定义或重定义一个var:

(def p "foo")                ;= #'user/p

求var的值:

p                            ;= "foo"

不求值,用var操作取得变量本身:

(var p)                      ;= #'user/p

注意回显的#'user/p,这其实是var引用的语法糖,可以直接用#'变量名引用var:

#'p                          ;= #'user/p

本地绑定:let

绑定了以后值就不可以变了:

(defn hypot [x y]
  (let [x2 (* x x)
        y2 (* y y)]
    (Math/sqrt (+ x2 y2))))

有的时候并不需要绑定的值,只是需要操作的副作用,那么就绑定到下划线_

(let [location (get-location)
      _        (println "location is: " location)]  ;; 只要打印效果,不要值
  (do-something))

解构

顺序解构

  • Clojure原生的listvector以及seq
  • 实现了java.util.List
  • Java数组。
  • 字符串。
(def v [42 "foo" 99.2 [5 12]])

以下方法是所有Clojure顺序集合都有的:

(first  v       )     ;; 42      
(first  (last v))     ;; 5       最后一个元素里的第一个,注意顺序
(second v       )     ;; "foo"  
(last   v       )     ;; [5 12] 
(nth    v      2)     ;; 99.2    取集合v的第2个元素
(.get   v      2)     ;; 99.2    java.util.List.get方法
(v      2       )     ;; 99.2    vector类的下标方法方法

let中用[...]绑定顺序集合:

(def v [42 "foo" 99.2 [5 12]])

(let [[x y z] v]
  (println x)                      ;; 42
  (println y)                      ;; foo
  (println z))                     ;; 99.2

相当于:

(let [x (nth v 0)
      y (nth v 1)
      z (nth v 2)]
  (println x)(println y)(println z))

解构可以嵌套,用_绑定用不到的元素:

(def v [42 "foo" 99.2 [5 12]])

(let [[x _ _ [y z]] v]
  (println x)               ;; 42
  (println y)               ;; 5
  (println z))              ;; 12

& 变量名绑定「剩下」的元素,这种形式用来递归与loop非常方便。 但要注意绑定的结果的类型是序列:

(def v [42 "foo" 99.2 [5 12]])

(let [[x & rest-elems] v]
  (println rest-elems))     ;; (foo 99.2 [5 12])

:as 变量名绑定原始集合:

;; 假设(some-function ...)的执行结果为:
;; [42 "foo" 99.2 [5 12]]

(let [orig-vector (some-function ...) ;; 要先绑定一次执行结果为orig-vector
      [x _ z] orig-vector]            ;; 这样函数体内才能调用这个结果
  (println orig-vector)               ;; [42 foo 99.2 [5 12]]
  (println x)                         ;; 42
  (println z))                        ;; 99.2

;; 可以用`:as 变量名`直接绑定到变量:
(let [[x _ z :as orig-vector] (some-function ...)]
  (println orig-vector)               ;; [42 foo 99.2 [5 12]]
  (println x)                         ;; 42
  (println z))                        ;; 99.2

map的解构

  • clojure原生的hash-maparray-map、记录类型。
  • 实现了java.util.Map的对象。
  • 任何支持get方法的对象,如:Clojure的vector、字符串、数组。

let中用{...}绑定映射:

(def m {:a 5 :b 6
        :c [7 8 9 ]
        :d {:e 10 :f 11}
        "foo" 88 42 false})  ;; 字符串与数字也可以作为key

(let [{a :a b :b} m]
  (println a)             ;; 5
  (println b))            ;; 6

解构非关键字类型的key:

(let [{f "foo" n 42} m]
  (println f)             ;; 88
  (println n))            ;; false

如果用{...}解构的目标是vector、字符串、数组,那么由于get方法的多态性, key为代表下标的数字。这种情况在取矩阵操作时非常常见, 因为用序列解析矩阵要写很条一串:

(let [{a 3 b 8} [11 22 33 44 55 66 77 88 99]]
  (println a)             ;; 44
  (println b))            ;; 99 

也可以嵌套地解构,注意操作顺序与结构顺序:

(def m {:c [7 8 9 ]
        :d {:e 10 :f 11}})

(let [{{e :e} :d} m]
  (println e))             ;; 10

映射解构与顺序解构结合:

(def m {:c [7 8 9 ]
        :d {:e 10 :f 11}})

(let [{[x _ y] :c} m]
  (println x)             ;; 7
  (println y))            ;; 9

也可以用:as 变量名把中间结果绑定到变量:

(def m {:c [7 8 9 ]
        :d {:e 10 :f 11}})

(let [{[x _ y :as c] :c} m
      {{e :e  :as d} :d} m]
  (println d)                ;; {:e 10, :f 11}
  (println c)                ;; [7 8 9]
  (println e)                ;; 10
  (println x)                ;; 7
  (println y))               ;; 9

or给变量绑定一个默认值,注意是通过「变量名」指定默认值而不是用key:

(def m {:a 5 :b 6})

(let [{c :c    ;; m中不存在`:c`
       d :d    ;; m中不存在`:d`
       a :a
       b :b
       :or {c 50 d 60}} m]   ;; 提供默认值,以变量名指定,而不是key
  (println a)                ;; 5
  (println b)                ;; 6
  (println c)                ;; 50
  (println d))               ;; 60

使用:keys直接把关键字绑定为同名的变量:

(def m {:a 5 :b 6})

(let [{a :a b :b} m]
  (println a)                ;; 5
  (println b))               ;; 6

;; 简写为:

(let [{:keys [a b]} m]
  (println a)                ;; 5
  (println b))               ;; 6

使用:strs直接把字符串绑定为同名的变量:

(def m {"a" 5 "b" 6})

(let [{a "a" b "b"} m]
  (println a)                ;; 5
  (println b))               ;; 6

;; 简写为:

(let [{:strs [a b]} m]
  (println a)                ;; 5
  (println b))               ;; 6

使用:syms直接把符号绑定为同名的变量:

(def m {'a 5 'b 6})

(let [{a 'a b 'b} m]
  (println a)                ;; 5
  (println b))               ;; 6

;; 简写为:

(let [{:syms [a b]} m]
  (println a)                ;; 5
  (println b))               ;; 6

之前介绍过& 变量名可以把顺序集合的剩余部分生成一个集合。 在有些情况下,生成的集合还可以被作为map再次解构。

前两个元素作为序列解构,后四个元素视为map解构:

(def user-info ["u8990" "1984-08-09" :name "Bob" :city "Shanghai"])

(let [[id birthday & {:keys [name city]}] user-info]
  (format "%s %s %s %s" id birthday name city))
;; "u8990 1984-08-09 Bob Shanghai"

整个顺序结构都可以作为map的,整个视为map解构:

(def user-info [:id "u8990" :birthday "1984-08-09" 
                :name "Bob" :city     "Shanghai"])

(let [[ & {:keys [id birthday name city]}] user-info]
  (format "%s %s %s %s" id birthday name city))
;; "u8990 1984-08-09 Bob Shanghai"

函数

定义函数

定义函数:

(fn [x y z]         ;; 参数,绑定方式与let相同
  (+ x y z))        ;; 函数体,包含do的特性

定义函数,并用参数调用:

((fn [x y z]
    (+ x y z))
  3 4 12)           ;; 实参

等价于:

(let [x 3 y 4 z 12]
  (+ x y z))

使用def绑定函数到变量,这样以后可以多次调用:

(def add-3-num
     (fn [x y z] (+ x y z)))   

(add-3-num 3 4 12)
(add-3-num 1 2 3)

函数可以有多个参数列表,多个参数列表的作用类似于重载:

(def strange-adder
     (fn self-ref                  ;; 可选的函数名,只有内部可见,用来调用自身
         ([x y] (+ x y))           ;; 参数列表2:当有一个实参时调用
         ([x]   (self-ref 1 x))))  ;; 参数列表1:当有两个实参时调用

(strange-adder 10)
(strange-adder 10 50)

带名字的过程可以更加方便地用defn来定义:

(defn strange-adder ([x y] (+ x y))
                    ([x]   (strange-adder 1 x)))

相当于:

(def strange-adder (fn strange-adder
                     ([x y] (+ x y))
                     ([x]   (strange-adder 1 x))))

只有一个参数列表的情况下可以省略一对括号:

(defn add-3-num [x y z]
  (+ x y z))   

带名称的函数可以用来调用自身或实现递归。更复杂的情况是多个函数相互调用, 这时可以用lefn定义多个具名函数,并让它们相互引用:

(letfn [(odd? [n]
          (if (zero? n)
            false
            (even? (dec n))))
        (even? [n]
          (or (zero? n)
              (odd? (dec n))))]
  (odd? 11))

参数解构

可变参数

和列表一样用&。下面的函数可以一个参数都没有:

(defn make-user [& [user-id]]
  {:user-id (or user-id
                (str (java.util.UUID/randomUUID)))})


(make-user)            ;; {:user-id "52861887-aca9-47d2-b767-f57ea802de8e"}
(make-user "user001")  ;; {:user-id "user001"}

关键字参数

例子:接收参数,然后组成一个map代表一条用户记录:

(defn make-user
  ;; username必写,其他的参数可选,join-date有默认值
  [username & {:keys [email join-date]
               :or   {join-date (java.util.Date.)}}]
  {:username username :join-date join-date :email email
   ;; 2.592e9 -> one month in ms
   :exp-date (java.util.Date. (long (+ -2.592e9 (.getTime join-date))))})

(make-user "Bobby")

(make-user "Bobby" 
           :join-date (java.util.Date. 111 0 1) 
           :email     "bobby@example.com")

函数字面量

#(...)形式定义函数字面量:

#(Math/pow %1 %2)

;; 相当于:

(fn [x y] (Math/pow x y))

注意函数字面量没有隐式的do特性,要显式地定义:

#(do (println (str %1 \^ %2))
     (Math/pow %1 %2))

;; 相当于:

(fn [x y]
  (println (str x \^ y))
  (Math/pow x y))
(#(do (println (str %1 \^ %2))
      (Math/pow %1 %2))
  2 3)

;; 相当于:

((fn [x y]
  (println (str x \^ y))
  (Math/pow x y))
  2 3)
  • 只有一个参数时,可以用%来表示,参数个数一定要和序号匹配。如果有4个参数, 就一定要在函数体中用到%4
  • 不定长参数用%&表示剩余参数。
#(- % (apply + %&))           ;;不定长参数

;; 相当于:

(fn [x & rest]
  (- x (apply + rest)))

fn函数可以嵌套,但是函数字面量不能嵌套:

(fn [x]                ;; 合法
  (fn [y]
    (+ x y)))

#(#(+ % %))            ;; 非法

条件判断

  • nil或非false的都是true
  • 没有定义else的if语句,检验为false时表达式为nil
(if 42 \t)                  ;; \t

(if (not true) \t)          ;; nil

谓词true?false?仅仅检查值是否匹配\t\f,不作逻辑判断:

(true? "string")            ;; false
(if    "string" \t \f)      ;; \t

循环:loop和recur

  • loop隐含let的特性,以向量为参数,有返回值。
  • recur跳转到最近的外层loop
  • recur在底层控制程序跳转,不会消耗的堆栈。用来高性能地实现循环和递归。
(loop [x 5]
  (if (neg? x)
    x
    (recur (dec x))))         ;; 跳回loop

;= -1

函数也可以用recur跳回函数头:

(defn countdown [x]
  (if (zero? x)
    :blastoff!                 ;; return key `:blastoff`
    (do (println x)
        (recur (dec x)))))

(countdown 5)
; 5
; 4
; 3
; 2
; 1
;= :blastoff!