导读
本文的目标是为熟悉 Ruby、Python或者其他类似语言、并对 Lisp 或者函数式编程有一定程度了解的程序员写的 Clojure 快速入门指南。
为了让文章尽可能地精炼且简单易懂,本文有以下三个特点:
一:不对读者的知识水平作任何假设,当遇上重要的知识点时,只给出 wikipedia 等网站的链接引用而不对知识点进行解释,有需要的读者可以沿着链接查看,没需要的直接略过就行了。
二:和第一条类似,没有介绍所有 Clojure 的语法和库,但会给出详细资料的引用链接。
三:将 Clojure 中的各项语法和其他常用语言,比如 Ruby 、 Python 和 JAVA 作类比,这样可以帮助有经验的读者快速了解 Clojure 的各项功能(尽管它们在实现细节和真正概念上可能有区别)。
阅读完本文后,你应该可以对 Clojure 有所了解,并熟悉一些用 Clojure 写程序的惯用法。
安装并运行 Clojure
Clojure 运行在 JRE (JAVA Runtime Environment) 之上,因此,你需要先安装 JRE ,然后到 Clojure 的主页下载最新版的 Clojure 。
安装 JRE 和 Clojure 的方法因使用的系统而不同,如果你和我一样使用 Archlinux ,那么执行命令 sudo pacman -S jre clojure 即可,其他系统可以按照 JAVA 主页 和 Clojure 主页上的方法来操作:
安装 JRE: http://www.oracle.com/technetwork/java/javase/downloads/index.html
安装 Clojure :http://clojure.org/getting_started
如果一切正常,那么现在你应该可以使用命令 clj 来调出 Clojure 的 REPL 程序了(在你的电脑上使用的命令可能有不同),用 Clojure 跟大家说声好吧:
$ clj Clojure 1.3.0 user=> (str "Hello world!") "Hello world!"
定义变量
Clojure 中的变量通过 def 来定义:
user=> (def greet "Good Morning") #'user/greet user=> greet "Good Morning"
在上面的代码中我们定义了一个 greet 变量,将它和字符串 "Good Morning" 绑定,类似的 Python 代码是:
greet = "Good Morning"
一个变量可以重复地绑定:
user=> (def lucky-number 10086) #'user/lucky-number user=> lucky-number 10086 user=> (def lucky-number 123123) #'user/lucky-number user=> lucky-number 123123
上面的代码用同一个变量进行了两次绑定,注意,虽然在同一段程序里反复使用一个同名变量在 Ruby 或者 Python 之类的语言中非常常见(赋值),但这种用法在 Clojure 中并不是一个好习惯(原因我迟些会告诉你),上面的代码只是告诉你可以这么做,并不是推荐你写这样的代码。
分隔符
你可能已经注意到了,在上面的 lucky-number 例子中,我们使用中划线 "-" 作为字母的分隔符,而不是 Ruby 和 Python 中常用的下划线 “_" ,的确如此,这是一个 Clojure 的惯用法。
下面是一些 Python 的变量名:
selected_elements
get_record_by_id
show_me_your_money
它们在 Clojure 中的写法是:
selected-elements get-record-by-id show-me-your-money
定义函数
定义函数的方式和定义变量很相似,不过定义函数使用的是 defn ,而不是 def。
比如现在我们要定义一个更先进的问候语系统,它可以根据你输入的问候语而做出不同的反应 —— 如果你输入 “Good Morning!”,它就返回 "Morning!" ,对于其他情况,它返回 “Hello!" :
user=> (defn greet-replay [you-say] (if (= you-say "Good Morning!") "Morning!" "Hello!")) #'user/greet-replay user=> (greet-replay "Hi!") "Hello!" user=> (greet-replay "Hello, huangz!") "Hello!" user=> (greet-replay "Good Morning!") "Morning!"
让我们一行行分析 greet-replay 函数:
首先,第一行,我们用 defn 定义了一个叫 greet-replay 的函数,它接受一个参数 you-say ,其中,参数被方括号所包围。
然后在第二行,greet-replay 函数使用了 if 形式(form),它和其他很多语言的 if 一样,都是接受一个布尔值,然后根据布尔值的真假来决定执行哪一个分支。
在这里,我们使用了代码 (= you-say "Good Morning!") 对比输入的参数和 "Good Morning!" 是否相等,如果相等,那么返回 "Morning!" ,否则的话,返回 "Hello!" 。
这里给出一个 Python 写的 greet-replay 函数作为参考:
>>> def greet_replay(you_say):
... if you_say == "Good Morning!":
... return "Morning!"
... else:
... return "Hello!"
...
>>> greet_replay("Hi!")
'Hello!'
>>> greet_replay("Good Morning!")
'Morning!'
可以看出, 两个版本除了在分隔符方面的差别之外,还有两点比较明显的不同:
在第二点方面, Ruby 和 Clojure 是一样的:
irb(main):005:0> def greet_replay(you_say)
irb(main):006:1> if you_say == "Good Morning!"
irb(main):007:2> "Morning!"
irb(main):008:2> else
irb(main):009:2* "Hello!"
irb(main):010:2> end
irb(main):011:1> end
=> nil
irb(main):012:0> greet_replay("Hi")
=> "Hello!"
irb(main):013:0> greet_replay("Good Morning!")
=> "Morning!"
前序操作符
你可能已经注意到,在上面的 greet_replay 函数中,我们对比两个字符串的方式和 Ruby 和 Python 有些不同,我们将 = 号放在前面:
(= you-say "Good Morning!")
它和 Python 或者 Ruby 对比的方法都不同:
if you_say == "Good Morning!"
我们称 Clojure 所使用的方式称之为前序操作符,而 Python 和 Ruby 所使用的方式称为中序操作符。
在 Clojure 中,我们总使用前序操作符 —— 因为 Clojure 没有操作符,只有函数、特殊形式(special form)和宏,当一个函数/特殊形式/宏被使用的时候,它总是被放在表达式的第一个位置上,用作前序操作符。
比如在上面的例子中,Clojure 的 = 函数完成的就是 Python 的 == 操作符的工作:对一个字符串进行对比。
谓词函数
在之前的 greet-replay 函数里,我们使用了 = 函数来测试两个字符串是否相等,继而决定 if 的最终走向。
这种测试并返回 true 或者 false 的对比,我们一般称之为谓词,或者分支判断,在 Clojure 中,谓词一般在最后加一个问号 "?" 作为标识,这也是一个 Clojure 惯用法。
比如说,我们可以将这个测试抽象成一个新的函数 same-greeting?
user=> (defn same-greeting? [you-say i-want] (= you-say i-want)) #'user/same-greeting? user=> (same-greeting? "Hi!" "Morning!") false user=> (same-greeting? "Hi!" "Hi!") true
然后可以使用新的 same-greeting? 重写之前的 greet-replay 函数:
user=> (defn greet-replay [you-say] (if (same-greeting? you-say "Good Morning!") "Morning!" "Hello!")) #'user/greet-replay user=> (greet-replay "Hi!") "Hello!" user=> (greet-replay "Good Morning!") "Morning!"
谓词函数增强了代码的可读性,现在的 greet-replay 函数读起来就像一句普通的英语一样,因为这个原因,在 Clojure 的标准库大量使用了谓词函数,比如 false? 、 nil? 、 sorted? 、 zero? ,等等。
使用 Ruby 的读者应该对带问号的谓词函数非常熟悉,因为 Clojure 和 Ruby 的问号惯用法都同样遗传自 Lisp 。
阶乘函数
在前面的介绍中,我们用函数写了一个简单的 greet-replay ,这一次,让我们用函数做一点更复杂的事情:计算阶乘。
阶乘是一个数学定义,它可以用符号 N! 表示,代表这样一个概念:计算从 1 开始,到某个数 N 的所有数的乘积。
比如说, 5! = 1 * 2 * 3 * 4 * 5 = 120 ,而 10! = 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 = 3628800,等等。
将这个概念进一步泛化,我们可以写出一个函数 factorial (阶乘) ,它接受一个参数 n ,并计算出这样一个乘法序列: 1 * 2 * 3 * ... * (n-1) * n 。
根据公式,可以很快地给出一个 Python 版本的 factorial 函数:
>>> def factorial(n):
... result = 1
... i = 1
... while i <= n:
... result *= i
... i += 1
... return result
...
>>> factorial(5)
120
>>> factorial(10)
3628800
上面的 factorial 定义了两个变量 i 和 result ,然后使用 while 迭代计算出阶乘。
很明显,如果我们想要在 Clojure 中计算阶乘函数,那么一个类似 Python 中的 while 关键字那样可以进行迭代的功能就是必不可少的 —— Clojure 中的确有类似的东西,它就是 loop 形式。
以下是一个使用 loop 形式写的阶乘函数:
user=> (defn factorial [n] (loop [i 1, result 1] (if (> i n) result (recur (inc i) (* i result))))) #'user/factorial user=> (factorial 5) 120 user=> (factorial 10) 3628800
嗯,这个 factorial 函数有点复杂,需要花些时间解释一下:
第一行是我们的老朋友 defn ,它定义一个名为 factorial 的函数,factorial 函数只接受一个参数 n 。
第二行是我们的新朋友,loop ,它和 defn 的使用方式有点类似,同样都是使用一个大括号将一些东西包围起来,这里是 [ i 1, result 1] ,这是什么意思呢?嗯,这就是说,要在 loop 形式之内,构建两个新的临时变量 i 和 result ,它们两个的值都是 1 。这些临时变量只能用在 loop 包围的地方。
第三行是我们的另一个老朋友 if ,它判断如果变量 i 比 参数 n 还要大的时候,就返回变量 result 作为函数的值。
第五行是 factorial 函数的关键,整个语句是 (recur (inc i) (* i result)) ,其中, recur 形式在 loop 形式当中被使用的时候,它会跳到 loop 所在的地方,并用 recur 参数里的值去更新 loop 形式里面的值。
比如说,当我们执行 (factorial 5) 的时候,factorial 内部的执行序列是这样的:
递归
"坑爹“,你可能会这样想,”huangz 这只菜鸟完全不会写 Python 代码, factorial 函数应该这样写才对:“
>>> def factorial(n):
... result = 1
... for i in range(1, n+1):
... result *= i
... return result
...
>>> factorial(5)
120
>>> factorial(10)
3628800
你是对的, factorial 这么写更简洁一些(事实上,这个写法在 n 很大的时候会出现性能问题),但是,那样的话,对比一看,我们忽然发现一个严重的问题: 解决同一个问题, Clojure 使用的代码居然比 Python 要复杂!
这怎么可能!?牛人们都说 Lisp 是世界上最强大的语言,那为什么解决这么一个简单的阶乘问题, Clojure 居然干不过 Python ,是什么地方出了问题呢?
嗯,实际上,在上面的定义阶乘函数的问题上,造成 Clojure 的解法比 Python 更复杂的原因,是因为我们没有使用 Clojure 去思考。
Clojure 是一门函数式语言,它和常用的语言比如 Python 或者 Ruby 有一些类似的地方,但是本质上 Clojure 和 Ruby 或者 Python 都非常不同 —— 比如说,在 Python 和 Ruby 中, 我们经常使用 for 关键字和 each 方法对一个对象(列表,集合,数组,等等)进行遍历,这种遍历是以迭代的方式进行的,但是,在 Clojure 中,人们更愿意使用递归而不是迭代。
什么是递归?简单说来,就是一个函数可以通过调用它自身来解决问题。
举个例子, 以下是 Clojure 递归版的阶乘函数,用它和之前的 loop 版本或者 Python 的 for 版本比较,应该能帮助你理解递归是怎么一回事:
user=> (defn factorial [n] (if (= n 1) 1 (* n (factorial (dec n))))) #'user/factorial user=> (factorial 5) 120 user=> (factorial 10) 3628800
上面的 factorial 比之前写过的两个版本都更简洁、更容易让人理解。
并且,要注意到,它和迭代版本使用的是不同的公式:
factorial(n) = 1 if n == 1
factorial(n) = n * (factorial n-1) if n > 1
这个公式和之前的 n! = 1 * 2 * 3 * ... * (n-1) * n 计算出的结果完全相同(本质上是一样的),但新的公式是递归地定义的,旧公式则不是 —— 新旧公式的区别,大概就是递归思考和迭代思考的区别,根据两种不同的思考方式,我们写出了完全不同的函数。
记住,要成为 Clojure 高手,你必须先加入递归俱乐部!
递归俱乐部有两条入门规则:
user=> (defn factorial [n] (reduce * (range 1 (inc n)))) #'user/factorial user=> (factorial 5) 120 user=> (factorial 10) 3628800
user=> (range 1 (inc 10)) (1 2 3 4 5 6 7 8 9 10)
(reduce * (1 2 3 4 5)) (* 1 (reduce * (2 3 4 5))) (* 1 (* 2 (reduce * (3 4 5)))) (* 1 (* 2 (* 3 (reduce * (4 5))))) (* 1 (* 2 (* 3 (* 4 (reduce * (5)))))) (* 1 (* 2 (* 3 (* 4 5)))) (* 1 (* 2 (* 3 20))) (* 1 (* 2 60)) (* 1 120) 120