Clojure概览
Clojure Reader
可以视为序列化工具:
(pr-str [1 2 3]) ;= "[1 2 3]"
反序列化工具:
(read-string "42") ;= 42 (read-string "(+ 1 2)") ;= (+ 1 2)
相对的还有pr
和read
函数,区别是这两个函数不是用字符串,而是用输入输出流
进行序列化和以序列化。
标量字面量
Nil
类似Java中的null
,在逻辑上作为false
。
Boolean
true
和false
。
字符串
默认就可以跨行:
"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
。
常用进制:41
、0xff
、040
任意进制:格式为BrN
,B
为进制,r
为值:
-
二进制的
2r111
为7
-
十六进制
16rFF
表示255
。
BigInt
任意精度的整数clojure.lang.BigInt
:42N
BigDecimal
任意精度的浮点数java.math.BigDecimal
:0.001M
有理数
有理数clojure.lang.Ratio
:22/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.Pattern
和java.util.regex
中的方法,
还可以用Clojure中提供的更加方便的:re-seq
、re-find
、re-matches
和
clojure.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]))
fn
、let
、loop
、try
、defn
和它们的变种都隐式包含了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原生的
list
、vector
以及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-map
、array-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!