Clojure 快速入门指南:1/3

导读

 

本文的目标是为熟悉 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!'
  

可以看出, 两个版本除了在分隔符方面的差别之外,还有两点比较明显的不同:

 

  1. Clojure 的 if 没有 else , Clojure 中 if 的两个分支只用空白或空行隔开即可。
  2. Clojure 将函数执行的最后一个表达式的值作为函数的返回值,因此我们不必像 Python 那样显式地使用 return 。

在第二点方面, 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 内部的执行序列是这样的:

 

  1. 定义 i = 1, result = 1 ,因为 (> i n) 测试失败,所以 (recur (inc i) (* i result)) 被执行,并更新 loop 变量的值。
  2. 因为 recur 的作用,loop 的两个变量被更新,现在 i 和 result 分别的值为 i = 2, result = 2, 测试 (> i n) 再次失败,执行 recur 。
  3. 因为 recur 的作用,现在 i = 3, result = 6 ,测试 (> i n) 失败,执行 recur 。
  4. 变量更新,现在 i = 4, result = 24 , 测试 (> i n) 失败,recur 执行。
  5. 变量更新,现在 i = 5, result = 120 ,测试 (> i n) 失败, recur 执行。
  6. 变量更新,现在 i = 6, 测试 (> i n) 成功, result 被返回。
  7. 结果,(factorial 5) 的值为 120

 

递归

 

"坑爹“,你可能会这样想,”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 高手,你必须先加入递归俱乐部!

 

递归俱乐部有两条入门规则:

 

  1. 使用递归思考,写递归函数解决递归问题。
  2. 不使用像 loop 那样具有迭代思想的技术,并将它们视为优美代码的大敌。
牢记并在你的代码中实践这两条规则,你很快就能晋升成为递归俱乐部的正式会员,继而走上成为 Clojure 高手的光辉大道。。。

更上一层楼

我们定义的递归版本 factorial 函数虽然简单,但是实际上,还有一种更简单的解法 —— 使用 reduce 和 range 函数,我们可以将 factorial 的代码数减至两行:

user=> (defn factorial [n]
           (reduce * (range 1 (inc n))))
#'user/factorial

user=> (factorial 5)
120

user=> (factorial 10)
3628800
 
新版 factorial 非常酷,让我们看看它是怎么来的。

首先, range 函数负责生成一个从 1 到 n 的数值列表,类似于 Python 的 range 函数:

user=> (range 1 (inc 10))
(1 2 3 4 5 6 7 8 9 10)
 
然后,我们使用 reduce 和乘法 * ,先将列表整个展开成一个乘法计算序列,然后再进行收缩计算。

比如说,执行代码 (reduce * (range 1 (inc 5))) ,代码运行时的状态大概如下:

(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
 
最新版本的 factorial 函数使用的是和之前一样的公式(从展开的计算链条里应该能看出这一点),但新版的 factorial 函数 处在抽象层次的一个非常高的位置上,对比之前的递归版本以及 loop 版本,它考虑的细节最少,写出的代码也最少,因为它使用 Clojure 来思考,并使用了其中几个相当强大的技术: 递归、  高阶函数、 reduce(也叫fold) 和 列表。

一般来说,如果你以 正确的方式来写 Clojure 代码,最终得出的代码会非常少且紧凑 —— 这也是为什么人们喜欢使用 Clojure (以及其他函数式语言)编程的原因。

小结

这一章主要介绍了 Clojure 的变量和函数的定义,以及递归的使用,并在最后的例子中对高阶函数等强有力的函数式编程技术作了一个快速而简单的了解。

待续。。。

嗯, 关于 Clojure 还有很多很酷的地方需要讲讲,比如它的数据结构、宏、数据类型、怎样利用 JAVA 平台的优势、并发、包,等等,我会在以后的章节继续给各位介绍这些好玩的东西。









你可能感兴趣的:(Clojure)