02. 快速上手
快速上手
这一节让我们来了解一下那些“使用
其实
同像性
其实
(+ 1 1) ; 函数调用
;=> 2
(class ‘(+ 1 1)) ; 查看(+ 1 1)的类型
;=> clojure.lang.PersistentList
可以看到,
因此,学习
同向性带来的好处是我们可以像操作数据结构一样操作代码,这为
形式(Form)
在
数值类型
和绝大多数语言一样,
除了常见的整型和浮点数外,
(class 0.0000000000001M) ; 任意精度实数
;=> java.math.BigDecimal ; 它的实际类型
(class 999999999999999999N) ; 任意精度整数
;=> java.math.Bigint
除此之外,
(/ 1 3)
;=> 1/3 ; 除法的结果以分数形式表示
相较于浮点数,有理数类型不会损失精度,但会带来一定的性能损失。
数值计算
(+ 1 2) ;=> 3
(- 2 1) ;=> 1
(* 2 2) ;=> 4
(/ 4 2) ;=> 2
(* (+ 1 2) 3) ;=> 9
有趣的是,这些函数都是可以接受任意数量的参数的:
(+ 1 2 3 4) ;=> 10
(apply * [1 2 3 4]) ;=> 24
(- 1) ;=> -1
(+) ;=> 0
(*) ;=> 1
数值比较
数值比较在
(> 2 1) ; => true
(= 1 1) ;=> true
这些函数也可以接受任意参数,因此,我们可以用
(defn desc? [nums] (apply >= nums)) ; 定义函数desc?
;=> #'user/desc?
(desc? [4 3 2 1])
;=> true
注:在
布尔值与nil
布尔值只包含
!注意:在
条件判断
(if test then else?)
(if (= (+ 1 1) 2)
"Math still works today!"
(println "Never happens"))
;=> "Math still works today!"
与
(when (= (+ 1 1) 2)
(println "print something")
"Math still works today!")
;; print something
;=> "Math still works today!"
注意:
许多语言都有
(defn speak [x]
(cond
(= x :dog) "Woof!Woof!Woof!"
(= x :cat) "Mew~"
(= x :repeater) *1 ; *1是REPL的特殊变量,表示上一个在REPL中求得的值
:else nil)) ; :else会被求值为真,因此当上述条件都不满足时就会返回nil
(speak :dog) ; => "Woof!Woof!Woof!"
(speak :repeater) ; => "Woof!Woof!Woof!"
(speak :cat) ; => “Mew~”
(speak :repeater) ; => “Mew~”
(speak :monkey) ; => nil
(defn speak2 [x]
(case x
:dog "Woof! Woof! Woof!"
:cat "Mew~"
:repeater *1
nil)) ; 最后一行表示默认情况
符号与变量
(def myname "Khellendros")
;=> #'user/myname
myname
;=> "Khellendros"
变量的初始值不是必须的:
(def no-value)
no-value
#object[clojure.lang.Var$Unbound 0xcc0a548 "Unbound: #'user/no-value"]
使用
(let [myage 22, mygender :male]
(do (println "Name: " myname)
(println "Age: " myage)
(println "Gender: " mygender)))
;; Name: Khellendros
;; Age: 22
;; Gender: :male
;=> nil
myage
;=> CompilerException java.lang.RuntimeException: Unable to resolve symbol: myage in this context, compiling:(null:0:0)
关键字
关键字类似于符号,不同之处在于,符号通常都会引用其他事物(比如变量和函数名
:a-keywd
;=> :a-keywd
:1+!
;=> :1+!
关键字最常见的用法是充当关联结构(如
字符与字符串
在
(class \a)
;=> java.lang.Character
而字符串和
(class “Hello”)
;=> java.lang.String
字符串中间可以换行:
“Hello
world!”
;=> “Hello\nworld!”
(str 1) ;=> “1”
(str 1 2 nil 3) ;=> “123”
正则表达式
在
(def words #”\w+”)
也可以使用
(def words2 (re-pattern \\w+))
使用
(re-seq words “aaa bbb|ccc,ddd”)
;=> (“aaa” “bbb” “ccc” “ddd”)
我们可以在正则表达式中添加分组:
(def middle-part #"\w+\-(\w+)\-\w+")
;=> #'user/middle-part
(re-seq middle-part "aaa-AAA-aaa bbb-BBB-bbb ccc-CCC-ccc")
;=> (["aaa-AAA-aaa" "AAA"] ["bbb-BBB-bbb" "BBB"] ["ccc-CCC-ccc" "CCC"])
此时
复合类型
所有这些
也许你会觉得这样的做法十分低效
(+ 1 1)
;=> 2
在
[1 2 3]
;=> [1 2 3]
向量的一大特点是它支持高效的随机访问。虽然向量的底层不是数组(array
(nth [1 2 3] 1) ; => 2
(get [1 2 3] 1) ; => 2
向量本身也可以当做函数使用,效果等同于
([1 2 3] 1) ;=> 2
!但是注意,向量不能当成集合来使用,对向量使用
(contains? [1 2 3] 0) ;=> true
(def me {:name "Khellendros", :age 22, :gender :male})
;=> #’user/me
!注意:映射表中不能出现重复的键
使用
(get me :name)
;=> "Khellendros"
映射表也可以直接当成函数使用,效果等同于调用
(me :gender)
;=> :male
此外,关键字也能当成函数使用:
(:age me)
;=> 22
使用关键字作为函数对映射表进行查询,是
映射表的键可以是任意类型,同时同一个映射表的键的类型也可以各不相同:
(def mess-map {
:name "@#$$%",
[1 2 3] "aaa",
"?" 233 } )
;=> #'user/mess-map
(mess-map [1 2 3])
;=> "aaa"
(mess-map "?")
;=> 233
(def img-exts #{"jpg" "gif" "png" "bmp"})
;=> #'user/img-exts
集合通常用来判断其是否包含某个元素:
(contains? img-exts "jpg")
;=> true
集合自身也可以当做函数使用,效果等同于
(img-exts "jpg")
;=> "jpg"
(img-exts "txt")
;=> nil
解构
复合类型可以进行解构。复合类型的构造可以看做是将多个量聚合成一个,而解构则是构造的逆过程,可以将复合解构拆解成多个量。
解构可以在许多地方发生,这里先以上面提到过的
(def nums [1 2 3])
;=> #’user/nums
(let [[a b c] nums] ; 解构nums
(+ a b c))
;=> 6
可以看到,向量
我们可以只取列表的前
(def natural-nums (iterate inc 0)) ; 表示全体自然数
;=> #'user/natural-nums
(let [[a b c] natural-nums] (+ a b c))
;=> 3
在这里
如果要只区第
(let [[_ _ a] natural-nums] a)
;=> 2
下划线(_)是一个合法的符号名,使用下划线忽略某些我们不关心的值是一种惯用法。注意这里下划线被绑定了两次,因此它最终的值是
(let [[a b c :as all] nums] (str "Sum of: " all " = " (+ a b c)))
;=> "Sum of: [1 2 3] = 6"
除了向量以外,映射表也可以进行绑定:
(let [{name :name, age :age} me] (str name " is " age " years old."))
:=> "Khellendros is 22 years old."
这种要把键名打两遍的做法略显繁琐,我们可以使用
(let [{:keys [name age]} me] (str name “ is “ age “ years old.”))
:=> "Khellendros is 22 years old."
除了
loop,
函数
(defn hello ;定义函数
"Say hello to someone." ;文档说明
[name] ;函数参数
(str "Hello, " name "!")) ;函数体
;=> #'user/hello
(hello "World")
;=> "Hello, World!"
函数可以有多个参数列表和函数体,此时各个参数列表的参数数量需要各不相同:
(defn vec-of
([a] [a])
([a b] [a b]))
;=> #'user/vec-of
(vec-of 1)
;=> [1]
(vec-of 1 2)
;=> [1 2]
在函数的参数声明处也可以对参数进行解构:
(defn third [[_ _ x]] x)
;=> #'user/first3
(third natural-nums)
;=> 2
代码块
函数体只能包含一个形式,如果我们要执行多个表达式怎么办呢?使用特殊形式
(do
(println "first")
(println "second")
"not return"
"return")
;;first
;;second
;=> "return"
匿名函数
一些函数会使用一个回调函数作为参数,回调函数通常都只有一两行代码,如果我们懒得给它们起名字,可以使用匿名函数。匿名函数使用
(def double-n (fn [n] (* n 2)))
;=> #’user/double-n
(double-n 10)
;=> 20
匿名函数还有一种简写形式,称作原位函数:
(def double-n-2 #(* % 2))
;=> #’user/double-n-2
(double-n-2 10)
;=> 20
原位函数使用井号加括号(#( ))定义,其中
递归
(defn count-down [n]
(when (pos? n) ; 如果n是正数就继续执行,否则返回nil
(println n) ; 输出n
(recur (dec n)))) ; 递减n,然后递归调用count-down
;=> #'user/count-down
(count-down 10)
;;10
;;9
;;8
;;…
;;1
;=> nil
(defn count [start end]
(loop [n start, end end]
(when (< n end)
(println n)
(recur (inc n) end))))
;=> #'user/count
(count 0 10)
;; 0
;; 1
;; 2
;; …
;; 9
;=> nil
在
注意:
遍历
虽然没有传统意义上的
(doseq [n [1 2 3]] ; 将列表[1 2 3]中的每一个元素依次绑定到n
(println n))
;; 1
;; 2
;; 3
;=> nil
下面是功能相同的
List<Integer> nums = Arrays.asList(1, 2, 3);
for (int num : nums) {
System.out.println(num);
}
(doseq [m [1 2], n [3 4]]
(println
(str m " + " n " = " (+ m n))))
;;1 + 3 = 4
;;1 + 4 = 5
;;2 + 3 = 5
;;2 + 4 = 6
;=> nil
上述例子清晰的展示了
List<Integer> nums1 = Arrays.asList(1, 2);
List<Integer> nums2 = Arrays.asList(3, 4);
for (int m : nums1) {
for (int n : nums2) {
String tmp = m + " + " + n + " = " + (m + n);
System.out.println(tmp);
}
}
; 求笛卡尔积
(for [m [1 2], n [\a \b]] [m n])
;=> ([1 \a] [1 \b] [2 \a] [2 \b])
; 变换
(for [num [1 2 3 4]] (inc num))
;=> (2 3 4 5)
;过滤出偶数
(for [n (range 0 10) :when (even? n)] n)
;=> (0 2 4 6 8)
遍历映射表
对映射表进行遍历时,会将映射表转化为二维序列:
(seq me)
([:name "Khellendros"] [:age 22] [:gender :male])
因此我们可以像操作普通二维序列一样遍历映射表。
需要注意的是,
与Java 互操作
在
调用静态方法/ 静态字段
可以用
(Math/PI)
;=> 3.141592653589793
(Math/abs -1)
;=> 1
构造实例对象
(def nums (java.util.ArrayList.))
;(def nums (new java.util.ArrayList))
;=> #’user/nums
nums
;=> []
方法调用
使用
(.add nums 1)
;=> true
nums
;=> [1]
读取、设置字段
读取字段的方法是
(def point (java.awt.Point. 0 1))
;=> #’user/point
(.-x point)
;=> 0
使用
(set! (.-x point) 2)
;=> 2
(.-x point)
;=> 2
注:在
小试牛刀:index-of 函数
我们上面提到
(index-of [1 1 4 5 1 4] 4)
;=> (2 5)
(index-of [1 1 4 5 1 4] 0)
;=> ()
如果序列内包含我们想要查找的目标,
首先,我们需要将列表项与其对应的下标关联在一起。还记得我们之前定义的
(defn indexed-vec [vec]
(map vector natural-nums vec))
;=> #’user/indexed-vec
(indexed-vec [\a \b \c \d])
;=> ([0 \a] [1 \b] [2 \c] [3 \d])
(map inc [1 2 3])
;=> (2 3 4)
如果传递给
(map + [1 2 3] [3 2 1])
;=> (4 4 4)
同理,如果传入
而
(defn index-of [vec item]
(for [[index value] (indexed-vec vec) :when (= value item)] index) )
;=> #’user/index-of
(index-of [1 1 4 5 1 4] 4)
;=> (2 5)
(index-of [1 1 4 5 1 4] 0)
;=> ()
更进一步?
(pos [1 2 3 1] 1) ;=> (0 3)
(pos {:a 1, :b 2, :c 1} 1) ;=>(:a :c)
(pos #{1 2 3} 0) ;=> ()
要做到这一点,首先我们需要定义一个通用版本的
(defn indexed [xs]
(cond
(map? xs) (seq xs)
(set? xs) (seq xs)
:else (indexed-vec xs)))
(defn pos [xs item]
(for [[index value] (indexed xs) :when (= item value)] index))
再进一步,我们完全可以把
(defn pos-if [xs pred]
(for [[index value] (indexed xs) :when (pred value)] index))
;=> #’user/pos-if
(pos-if [1 2 3 1] #(> %1 1))
;=> (1 2)