严格来说,Lisp是一种多范式语言,不仅仅是函数式编程,也可面向对象,也可面向过程,但它的特性确实在函数式编程中更为出色些。Clojure作为Lisp家族中的一员,继承祖先的优良传统,也有自己的特色,特别在“函数是第一公民”的原则上。比如匿名函数的递归,在Common Lisp只能自定义宏实现,在Scheme优雅不少,但也好看不到哪去。


  • 定义有名字的函数

定义函数可以defn声明有名字的函数,最后一条表达式的结果作为返回值,defn后面是函数名,[]则是参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defn i_am_fn [x y]
(* x y))
;clojure的函数重载,只更参数个数有关,与参数类型无关。
(defn i_am_fn [x y z]
(println x "+" y "+" z "=")
(* x y z)
(+ x y z))
;或者可以这样写
(defn i_am_fn
([x y]
(* x y))
([x y z]
(println x "+" y "+" z "=")
(* x y z)
(+ x y z)))

  • 定义‘没有’名字的函数—匿名函数

定义匿名函数则用fn,同样最后一条表达式的结果作为返回值,fn后面也可以有函数名(可选),[]里是参数列表。

1
2
3
(fn [x y]
(println "i don't kown!!!")
(+ x y))

匿名函数也可以重载,这在匿名函数的递归或互相调用十分有用!我们来看看下面使用递归实现阶乘。

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
;方式一:
;先看给予匿名函数一个函数名,可以重载两个参数长度不一样的函数。
;factorial[x]调用factorial[x y],而factorial[x y]做尾递归,并返回给factorial[x]
;但这种写法有个缺点,就是同时暴露了[x]和[x y]两个函数,而我们只需要[x]即可
(fn factorial
([x]
(factorial (- x 1) x))
([x y]
(if (< x 1)
y
(factorial (- x 1) (* x y)))))
;方式二:
;也许我们可以用偏函数?将上面的factorial[x]抽取出来,为factorial[x y]再做一层匿名函数。
;#是函数的字面量
(#((fn factorial [x y]
(if (< x 1)
y
(factorial (- x 1) (* y x)))) % 1) 10)
;方式三:
;letfn是一种类似于let本地绑定的局部函数声明,可以声明多个本地函数,离开letfn则失效。
;形式上于第一种方式相似,但又有很大不同。
;1、声明的本地函数不一定是重载函数,你看第一种方式,并不能有两个参数长度相同的函数,而letfn允许,只需要函数名不同即可
;2、在最后的返回值,你可以选择暴露哪个函数,而其他函数则私有化
(letfn [(factorial [x]
(factorial_2 (- x 1) x))
(factorial_2 [x y]
(if (< x 1)
y
(factorial_2 (- x 1) (* x y))))]
#(factorial %))

上面有个细节需要注意,就是factorial的尾递归,在jvm上不是真正的尾递归,因为虚拟机并没有提供尾调用优化,与普通递归并无二样,以后会说到recur实现尾递归。

三种不同形式的匿名函数递归,显然第一种是最不可取的!论灵活性的话,我自己比较喜欢第三种,用letfn实现,递归中如果有其他独立的算法模块,可以单独作为一个私有方法,提高可读性。不过,若非必要,还是声明普通函数吧!

  • 函数参数的解构

参数列表的解构,与let大同小异,可以参考基本语法

先来看看函数可变参数,&字符后跟随的是除去前面参数所剩余的参数列表。

1
2
3
4
5
6
7
8
9
10
11
(defn i_am_fn [x & rest]
(apply str rest))
;rest则表示除第一个参数1,剩余的[2 3 4 5 6]
(i_am_fn 1 2 3 4 5 6);="23456"
;也可以表示可变参数
(defn i_am_fn_2 [& rest] ;也可以[& [rest]],这样函数至多接受一个参数
{:user-id (or rest
(java.util.UUID/randomUUID))})
(i_am_fn_2 "string" :keyname 123);={:user-id ("string" :keyname 123)}
(i_am_fn_2);={:user-id #uuid"d38aaef2-d5d2-4e86-850e-b29c18b870b5"}

有时候,函数的一些参数并非必要传入,clojure允许我们为参数设置默认值,而这些能被设置默认值的参数叫做关键字参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defn a_man [username & {:keys [email times]
;设置默认值,如果没有传入值的话,传nil是有效的。
:or {times (java.util.Date.) email "740762239@qq.com"}}]
{:username username :email email :time times})
;没有被设为关键字参数的参数,则必须要传入!例如username。
(a_man "i am man")
;={:username "i am man", :email "740762239@qq.com", :time #inst"2017-01-19T14:25:41.806-00:00"}
(a_man "i am man" :email "777@qq.com" :times "2017-01-17 21:23:53")
;={:username "i am man", :email "777@qq.com", :time "2017-01-17 21:23:53"}
;可能会有点不明白上面关键字参数,其实它就是map的解构
;等同于
;只不过,源于优雅,建议用:keys罢了
(defn a_man [username & {email :email
times :times
:or {times (java.util.Date.) email "740762239@qq.com"}}]
{:username username :email email :time times})