清单 5. 针对 Fibonacci 数的一个惰性序列
(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 所示。
清单 6. 生成 Fibonacci 数
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 数的方式,让我们来解决这个问题。
清单 7. 示例 2
(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 的一个简化版,其中使用了闭包。
清单 8. 简化的解决方案
(println (reduce + (filter even? (take-while (fn [n] (< n 4000000)) (lazy-seq-fibo))))) |
在清单 8 中,我们使用了 fn 宏。这会创建一个匿名函数并返回此函数。对函数使用断言通常很简单,而且最好使用闭包定义。而 Clojure 具有一种更为简化的方式来定义闭包。
清单 9. 简短的闭包
(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 问题。
清单 10. Project Euler 的问题 8
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,可用来在字符串和整数之间来回转换。作为开始,我们首先需要处理上面这一大段不规则的文本。
清单 11. 解析文本
(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 中完成。
清单 12. 创建一个数值序列
(def the-digits (map #(Integer. (str %)) (filter #(Character/isDigit %) (seq big-num-str)))) |
同样地,让我们从最里面的表达式开始。我们使用 seq 函数来将 big-num-str 转变为一个序列。不过,结果表明此序列并非我们所想要的。REPL 可以帮助我们看出这一点,如下所示。
清单 13. 查看这个 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... |