dragonzht 2016-01-19
Lisp 是一种编程语言,以表达性和功能强大著称,但人们通常认为它不太适合应用于一般情况。Clojure 是一种运行在 Java™ 平台上的 Lisp 方言,它的出现彻底改变了这一现状。如今,在任何具备 Java 虚拟机的地方,您都可以利用 Lisp 的强大功能。在本文中,了解如何开始使用 Clojure,学习它的一些语法,同时利用 Eclipse 的 Clojure 插件提供帮助。
本文介绍了 Clojure 编程语言。Clojure 是一种 Lisp 方言。本文假设您不熟悉 Lisp,但需要您具备 Java 技术方面的知识。要编写 Clojure 程序,需要 Java Development Kit V5 或更高版本以及 Clojure 库。本文使用的是 JDK V1.6.0_13 和 Clojure V1。此外,您还需要利用 Eclipse 的 Clojure 插件(clojure-dev),因此还要用到 Eclipse。在本文中,我们使用了 Eclipse V3.5 和 clojure-dev 0.0.34。
不久前,要想在 Java Virtual Machine (JVM) 上运行程序,还需要使用 Java 编程语言来编写程序。但那个时代已经一去不复返了。现在更多的选择,比如 Groovy、Ruby(通过 JRuby)及 Python (通过 Jython),带来了一种更为过程化的脚本式的编程风格,或者它们各自拥有独特的面向对象编程特色。这两种都是 Java 程序员所熟悉的。也许有人会说用这些语言与用 Java 语言编写程序没什么区别,只要习惯不同的语法就可以了。
虽然 Clojure 还不算是 JVM 的一种新的编程语言,但它与 Java 技术及前面提到过的其他任何 JVM 语言都有很大的区别。它是一种 Lisp 方言。从 20 世纪 50 年代开始至今,Lisp 语言家族已经存在很长时间了。Lisp 使用的是截然不同的 S-表达式或前缀 注释。这个注释可以被归结为 (function arguments...)
。通常总是从函数名开始,然后列出要向这个函数添加的零个或多个参数。函数及其参数通过圆括号组织在一起。数量众多的括号也成为了 Lisp 的一大特征。
您可能已经发现,Clojure 是一种函数式编程语言。专业人士可能会说它太过单一,但实际上它却囊括了函数式编程的所有精华:避免了不稳定状态、递归、更高阶的函数等。Clojure 还是一个动态类型的语言,您可以选择添加类型信息来提高代码中的关键路径的性能。Clojure 不仅可在 JVM 上运行,而且在设计上还兼顾了 Java 的互操作性。最后,Clojure 在设计上也考虑了并发性,并具有并发编程的一些独特特性。
对大多数人来说,学习一种新的编程语言的最佳方法是从练习编写代码开始。按照这个思路,我们将提出一些简单的编程问题,然后用 Clojure 来解决这些问题。我们将深入剖析每种解决方案以便您能更好地理解 Clojure 是如何工作的、该如何使用它、它最擅长什么。不过,像其他语言一样,要想使用 Clojure,我们需要先为它建立一个开发环境。幸好,建立 Clojure 环境非常容易。
要建立 Clojure 语言环境,您所需要的就是一个 JDK 和 Clojure 库(一个 JAR 文件)。开发和运行 Clojure 程序有两种常用方式。其中最常用的一种方法是使用它的 REPL(read-eval-print-loop)。
$ java -cp clojure-1.0.0.jar clojure.lang.Repl Clojure 1.0.0- user=>
此命令从 Clojure JAR 所在的目录运行。按需要,将路径调整到 JAR。您还可以创建一个脚本并执行此脚本。为此,需要执行一个名为 clojure.main 的 Java 类。
$ java -cp clojure-1.0.0.jar clojure.main /some/path/to/Euler1.clj 233168
同样,您需要将路径调整到 Clojure JAR 及脚本。Clojure 终于有了 IDE 支持。Eclipse 用户可以通过 Eclipse 升级网站来安装 clojure-dev 插件。安装完毕且确保处于 Java 透视图中后,就可以创建一个新的 Clojure 项目和新的 Clojure 文件了,如下所示。
有了 clojure-dev,您就能够获得一些基本语法的亮点,包括圆括号匹配(Lisp 所必需的)。您还可以在被直接嵌入到 Eclipse 的一个 REPL 中放入任意脚本。这个插件还很新,在本文写作之时,它的特性还在不断发展。现在,我们已经解决了基础设置的问题,接下来让我们通过编写一些 Clojure 程序来进一步研究这个编程语言。
Lisp 这一名字来自于 “列表处理”,人们常说 Lisp 中的任何东西都是一个列表。在 Clojure 中,列表被统一成了序列。在第一个示例中,我们将处理下述的编程问题。
如果我们要列出 10 以下且为 3 或 5 的倍数的所有自然数,我们将得到 3、5、6 和 9。这几个数的和是 23。我们的题目是求出 1,000 以下且为 3 或 5 的倍数的自然数的和。
这个题目取自 Project Euler,Project Euler 是一些可以通过巧妙(有时也不是很巧妙)的计算机编程解决的数学题集。实际上,这就是我们的问题 1。清单 3 给出了这个问题的解决方案,其中使用了 Clojure。
(defn divisible-by-3-or-5? [num] (or (== (mod num 3) 0)(== (mod num 5) 0))) (println (reduce + (filter divisible-by-3-or-5? (range 1000))))
第一行定义了一个函数。记住:函数是 Clojure 程序的基石。大多数 Java 编程员都习惯于把对象作为其程序的基石,所以一些人可能需要一些时间才能习惯使用函数。您可能会认为 defn
是此语言的关键字,但它实际上是个宏。一个宏允许您对 Clojure 做扩展以向该语言中添加新的关键字。也就是说,defn
并不是此语言规范的一部分,而是通过此语言的核心库被添加上的。
在本例中,第一行实际上是创建了一个名为 divisible-by-3-or-5?
的函数。这遵循了 Clojure 的命名约定。单词均以连字符分隔,并且此函数的名字是以一个问号结尾的,用以表示此函数是一个断言,因它会返回 true 或 false。此函数只接受一个名为 num
的单一参数。如果有更多的输入参数,它们将显示在这个方括号内,以空格分隔。
下面是这个函数的主体。首先,我们调用 or
函数。这是常用的 or
逻辑;它是一个函数,而不是一个操作符。我们将它传递给参数。而每个参数也是一个表达式。第一个表达式是以 ==
函数开始的。它对传递进来的这些参数的值进行比较。传递给它的有两个参数。第一个参数是另一个表达式;这个表达式调用 mod
函数。这是数学里的模运算符或 Java 语言里的 %
运算符。它返回的是余数,所以在本示例中,余数是 num
被 3 除后的余数。该余数与 0 比较(如果余数是 0,那么 num
可以被 3 整除)。同样地,我们检查 num
被 5 除后的余数是否是 0。如果这两种情况的余数有一个是 0,那么此函数返回 true。
在接下来的一行,我们创建一个表达式并把它打印出来。让我们从圆括号的最里面开始。在这里,我们调用了 range 函数并将数 1,000 传递给它。这会创建一个由 0 开始,所有小于 1,000 的数组成的序列。这组数正是我们想要检查是否可被 3 或 5 整除的那些数。向外移,我们会调用 filter
函数。此函数接受两个参数:第一个是另一个函数,该函数必须是一个断言,因它必须要返回 true 或 false;第二个参数是一个序列 — 在本例中,此序列是 (0, 1, 2, ... 999)
。filter
函数被应用到这个断言,如果该断言返回 true,序列中的元素就被加到此结果。这个断言就是在上一行中定义的 divisible-by-3-or-5?
函数。
因此,这个过滤器表达式的结果是一个整数序列,其中每个整数都小于 1,000 且能被 3 或 5 整除。而这也正好是我们感兴趣的那组整数,现在,我们只需将它们相加。为此,我们使用 reduce 函数。这个函数接受两个参数:一个函数和一个序列。它将此函数应用到序列中的前两个元素。然后再将此函数应用到之前的结果以及序列中的下一个元素。在本例中,该函数就是 +
函数,即加函数。它能将该序列中的所有元素都加起来。
从清单 3 中,不难看出在这一小段代码中发生了很多事情。而这也恰好是 Clojure 吸引人之处。发生的操作虽然很多,但如果熟悉了这些注释,代码将很容易读懂。同样的事情,若用 Java 代码去做,则需要用到更多的代码量。让我们接下来看看另一个例子。
通过这个例子,我们来探讨一下 Clojure 内的递归和惰性。这对于很多 Java 程序员而言是另一个新概念。Clojure 允许定义 “懒惰” 的序列,即其中的元素只有在需要的时候才进行计算。借此,您就可以定义无穷序列,这在 Java 语言中是从未有过的。要了解这一点是多么地有用,可以看看下面这个例子,该例涉及到了函数式语言的另一个十分重要的方面:递归。同样地,我们仍然使用 Project Euler 中的一个编程问题,但是这次,它是问题 2。
Fibonacci 序列中的每个新的项都是其前面两项相加的结果。从 1 和 2 开始,前 10 项将是:1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
现在,我们要找到此序列中所有偶数项之和,且不超过 400 万。为了解决这个问题,Java 程序员一般会想到要定义一个函数来给出第 n 个 Fibonacci 数。这个问题的一个简单实现如下所示。
(defn fib [n] (if (= n 0) 0 (if (= n 1) 1 (+ (fib (- n 1)) (fib (- n 2))))))
它检查 n 是否为 0;如果是,就返回 0。然后检查 n 是否为 1。如果是,就返回 1。否则,计算第 (n-1) 个 Fibonacci 数和第 (n-2) 个 Fibonacci 数并将二者加起来。这当然很正确,但是如果您已经进行了很多的 Java 编程,就会看到问题。像这样的递归定义很快就会填满��栈,从而导致堆栈溢出。Fibonacci 数形成了一个无穷序列,所以应该用 Clojure 的无穷惰性序列描述它,如清单 5 所示。请注意虽然 Clojure 具有一个更为有效的 Fibonacci 实现,是标准库(clojure-contrib)的一部分,但它较为复杂,因此这里所示的这个 Fibonacci 序列来自于 Stuart Halloway 的一本书。
(defn lazy-seq-fibo ([] (concat [0 1] (lazy-seq-fibo 0 1))) ([a b] (let [n (+ a b)] (lazy-seq (cons n (lazy-seq-fibo b n))))))
在清单 5 中,lazy-seq-fibo
函数具有两个定义。第一个定义没有参数,因此方括号是空的。第二个定义有两个参数 [a b]
。对于没有参数的情况,我们获取序列 [0 1]
并将它连到一个表达式。该表达式是对 lazy-seq-fibo
的一个递归调用,不过这次,它调用的是有两个参数的情况,并向其传递 0 和 1。
两个参数的情况从 let
表达式开始。这是 Clojure 内的变量赋值。表达式 [n (+ a b)]
设置变量 n
并将其设为等于 a+b
。然后它再使用 lazy-seq
宏。正如其名字所暗示的,lazy-seq
宏被用来创建一个惰性序列。其主体是一个表达式。在本例中,它使用了 cons
函数。该函数是 Lisp 内的一个典型函数。它接受一个元素和一个序列并通过将元素添加在序列之前来返回一个新序列。在本例中,此序列就是调用 lazy-seq-fibo
函数后的结果。如果这个序列不是惰性的,lazy-seq-fibo
函数就会一次又一次地被调用。不过,lazy-seq
宏确保了此函数将只在元素被访问的时候调用。为了查看此序列的实际处理,可以使用 REPL,如清单 6 所示。
1:1 user=> (defn lazy-seq-fibo ([] (concat [0 1] (lazy-seq-fibo 0 1))) ([a b] (let [n (+ a b)] (lazy-seq (cons n (lazy-seq-fibo b n)))))) #'user/lazy-seq-fibo 1:8 user=> (take 10 (lazy-seq-fibo)) (0 1 1 2 3 5 8 13 21 34)
take
函数用来从一个序列中取得一定数量(在本例中是 10)的元素。我们已经具备了一种很好的生成 Fibonacci 数的方式,让我们来解决这个问题。
(defn less-than-four-million? [n] (< n 4000000)) (println (reduce + (filter even? (take-while less-than-four-million? (lazy-seq-fibo)))))
在清单 7 中,我们定义了一个函数,称为 less-than-four-million?
。它测试的是其输入是否小于 400 万。在接下来的表达式中,从最里面的表达式开始会很有用。我们首先获得一个无穷的 Fibonacci 序列。然后使用 take-while
函数。它类似于 take
函数,但它接受一个断言。一旦断言返回 false,它就停止从这个序列中获取。所以在本例中,Fibonacci 数一旦大于 400 万,我们就停止获取。我们取得这个结果并应用一个过滤器。此过滤器使用内置的 even?
函数。该函数的功能正如您所想:它测试一个数是否是偶数。结果得到的是所有小于 400 万且为偶数的 Fibonacci 数。现在我们对它们进行求和,使用 reduce
,正如我们在第一个例子中所做的。
清单 7 虽然能解决这个问题,但是并不完全令人满意。要使用 take-while
函数,我们必须要定义一个十分简单的函数,称为 less-than-four-million?
。而结果表明,这并非必需。Clojure 具备对闭包的支持,这没什么稀奇。这能简化代码,如清单 8 中所示。
闭包在很多编程语言中非常常见,特别是在 Clojure 等函数语言中。这不仅仅是因为函数 “级别高” 且可被作为参数传递给其他函数,还因为它们可被内联定义或匿名定义。清单 8 是清单 7 的一个简化版,其中使用了闭包。
(println (reduce + (filter even? (take-while (fn [n] (< n 4000000)) (lazy-seq-fibo)))))
在清单 8 中,我们使用了 fn
宏。这会创建一个匿名函数并返回此函数。对函数使用断言通常很简单,而且最好使用闭包定义。而 Clojure 具有一种更为简化的方式来定义闭包。
(println (reduce + (filter even? (take-while #(< % 4000000) (lazy-seq-fibo)))))
我们曾使用 #
创建闭包,而不是借助 fn
宏。而且我们还为传递给此函数的第一个参数使用了 %
符号。您也可以为第一个参数使用 %1
,如果此函数接受多个参数,还可以使用类似的 %2
、%3
等。
通过上述这两个简单的例子,我们已经看到了 Clojure 的很多特性。Clojure 的另一个重要的方面是其与 Java 语言的紧密集成。让我们看另外一个例子来了解从 Clojure 使用 Java 是多么有帮助。
Java 平台能提供的功能很多。JVM 的性能以及丰富的核心 API 和以 Java 语言编写的第三方库都是功能强大的工具,能够帮助避免大量重复的工作。Clojure 正是围绕这些理念构建的。在 Clojure 中,很容易调用 Java 方法、创建 Java 对象、实现 Java 界面以及扩展 Java 类。为了举例说明,让我们来看看另一个 Project Euler 问题。
Find the greatest product of five consecutive digits in the 1000-digit number. 73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 85861560789112949495459501737958331952853208805511 12540698747158523863050715693290963295227443043557 66896648950445244523161731856403098711121722383113 62229893423380308135336276614282806444486645238749 30358907296290491560440772390713810515859307960866 70172427121883998797908792274921901699720888093776 65727333001053367881220235421809751254540594752243 52584907711670556013604839586446706324415722155397 53697817977846174064955149290862569321978468622482 83972241375657056057490261407972968652414535100474 82166370484403199890008895243450658541227588666881 16427171479924442928230863465674813919123162824586 17866458359124566529476545682848912883142607690042 24219022671055626321111109370544217506941658960408 07198403850962455444362981230987879927244284909188 84580156166097919133875499200524063689912560717606 05886116467109405077541002256983155200055935729725 71636269561882670428252483600823257530420752963450
在这个问题中,有一个 1,000-位的数字。在 Java 技术里,该数可以通过 BigInteger
表示。但是,我们无需在整个数上进行计算 — 只需每次计算 5 位。因而,将它视为字符串会更为简单。不过,为了进行计算,我们需要将这些数位视为整数。所幸的是,在 Java 语言中,已经有了一些 API,可用来在字符串和整数之间来回转换。作为开始,我们首先需要处理上面这一大段不规则的文本。
(def big-num-str (str "73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 85861560789112949495459501737958331952853208805511 12540698747158523863050715693290963295227443043557 66896648950445244523161731856403098711121722383113 62229893423380308135336276614282806444486645238749 30358907296290491560440772390713810515859307960866 70172427121883998797908792274921901699720888093776 65727333001053367881220235421809751254540594752243 52584907711670556013604839586446706324415722155397 53697817977846174064955149290862569321978468622482 83972241375657056057490261407972968652414535100474 82166370484403199890008895243450658541227588666881 16427171479924442928230863465674813919123162824586 17866458359124566529476545682848912883142607690042 24219022671055626321111109370544217506941658960408 07198403850962455444362981230987879927244284909188 84580156166097919133875499200524063689912560717606 05886116467109405077541002256983155200055935729725 71636269561882670428252483600823257530420752963450"))
这里,我们利用了 Clojure 对多行字符串的支持。我们使用了 str
函数来解析这个多行字符串。之后,使用 def
宏来定义一个常量,称为 big-num-str
。不过,将其转换为一个整数序列将十分有用。这在清单 12 中完成。
(def the-digits (map #(Integer. (str %)) (filter #(Character/isDigit %) (seq big-num-str))))
同样地,让我们从最里面的表达式开始。我们使用 seq
函数来将 big-num-str
转变为一个序列。不过,结果表明此序列并非我们所想要的。REPL 可以帮助我们看出这一点,如下所示。
big-num-str
序列user=> (seq big-num-str) (\7 \3 \1 \6 \7 \1 \7 \6 \5 \3 \1 \3 \3 \0 \6 \2 \4 \9 \1 \9 \2 \2 \5 \1 \1 \9 \6 \7 \4 \4 \2 \6 \5 \7 \4 \7 \4 \2 \3 \5 \5 \3 \4 \9 \1 \9 \4 \9 \3 \4 \newline...
REPL 将字符(一个 Java char)显示为 \c
。因此 \7
就是 char 7,而 \newline
则为 char \n(一个换行)。这也是直接解析文本所得到的结果。显然,我们需要消除换行并转换为整数,然后才能进行有用的计算。这也正是我们在清单 11 中所做的。在那里,我们使用了一个过滤器来消除换行。请再次注意,我们为传递给 filter
函数的断言函数使用了一个简短的闭包。闭包使用的是 Character/isDigit
。这是来自 java.lang.Character
的静态方法 isDigit
。因此,这个过滤器只允许数值 char,而会丢弃换行字符。
现在,消除了换行之后,我们需要进行到整数的转换。浏览清单 12,会注意到我们使用了 map
函数,它接受两个参数:一个函数和一个序列。它返回一个新的序列,该序列的第 n 个元素是将此函数应用到原始序列的第 n 个元素后的结果。对于这个函数,我们再次使用了一个简短的闭包注释。首先,我们使用 Clojure 的 str
函数来将这个 char 转变为字符串。我们为什么要这么做呢?因为,接下来,我们要使用 java.lang.Integer
的构造函数来创建一个整数。而这是由 Integer
注释的。应该将这个表达式视为新的 java.lang.Integer(str(%))
。联合使用它与 map
函数,我们就能如愿地得到一个整数序列。现在,我们来解决这个问题。
(println (apply max (map #(reduce * %) (for [idx (range (count the-digits))] (take 5 (drop idx the-digits))))))
要理解这段代码的含义,让我们从 for
宏开始。它不同于 Java 语言中的 for
循环,它是一个序列推导式。首先,我们使用一些方括号来创建一个绑定。在本例中,我们将变量 idx
绑定到一个从 0 到 N-1 的序列,其中 N 是序列 the-digits
中的元素的数量(N = 1,000,因为原始的数值具有 1,000 位)。接下来,for
宏获取一个表达式来生成一个新的序列。它将迭代 idx
序列中的每个元素,求得表达式的值,并将结果添加到这个返回序列。不难看出,这在某些方面充当了 for
循环的功能。在此推导式中所使用的表达式首先使用 drop
函数来向下移(drop)此序列的第一个 M 元素,然后使用 take
函数来取得此截短序列的前五个元素。M 将是 0,然后是 1,2,以此类推,所以结果将是一个包含序列的序列,其中的前五个元素将是 (e1, e2, e3, e4, e5),下一个元素将是 (e2, e3, e4, e5, e6),以此类推,其中的 e1、e2 等均是来自 the-digits
的元素。
有了这个包含序列的序列之后,我们就可以使用 map
函数了。我们使用 reduce
函数将五个数组成的每个序列转换成这五个数的乘积。现在,我们就得到了一个整数序列,其中的第一个元素是元素 1-5 的乘积,第二个元素是元素 2-6 的乘积,以此类推。我们想要得到最大的乘积。为此,我们使用 max
函数。然而,max
一般接受传递给它的多个元素,而不是单一一个序列。为了将这个序列转变为多个元素后再传递给 max
,我们使用 apply
函数。这会产生我们想要的最大数,当然还会打印出结果。现在,您已经解决了几个问题,并同时掌握了该如何使用 Clojure。
在本文中,我们介绍了 Clojure 编程语言并从 Eclipse 的 Clojure 插件的使用中受益匪浅。我们先是简单查看了其原理和特性,之后重点介绍了几个代码示例。在这些代码示例中,我们逐渐了解了该语言的核心特性:函数、宏、绑定、递归、惰性序列、闭包、推导式以及与 Java 技术的集成。Clojure 还有很多其他的方面。我希望,该语言已经吸引了您,以至于您有兴趣借助本文所列的一些参考资料来深入了解它。
Clojure 的详细介绍:请点这里
Clojure 的下载地址:请点这里