我同意Pavel的观点,什么是直觉性是主观的。因为我(慢慢地)开始理解Haskell,所以即使我从未写过Clojure代码,我也能知道它的功能是什么。因此,我认为Clojure这一行代码相当直观,因为我之前见过它,并正在适应更加函数式的思维方式。
让我们考虑数学定义,好吗?
{ 0 if x = 0 }
F(x) = { 1 if x = 1 }
{ F(x - 1) + F(x - 2) if x > 1 }
从格式上来说,这不是最理想的——那三个对齐的括号应该成为一个巨大的括号——但是谁在乎呢?对于大多数具有数学背景的人来说,这是一个很清晰的斐波那契数列定义。让我们看看用Haskell写的相同内容,因为我比Clojure更熟悉它:
fib 0 = 0
fib 1 = 1
fib n = fibs (n - 1) + fibs (n - 2)
这是一个函数fib
,返回第n个斐波那契数。跟Python或Clojure中的不完全一样,所以让我们来修正一下:
fibs = map fib [0..]
这使得fibs
成为一个无限的斐波那契数列。 fibs !! 1
是1,fibs !! 2
是1,fibs !! 10
是55等等。然而,即使在像Haskell这样依赖于高度优化递归的语言中,这种方法可能仍然相当低效。让我们看看在Haskell中定义的Clojure:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
前几个字符很简单:0 : 1 :
创建了一个由元素0和1组成的列表,然后是一些其他内容。但是剩下的所有内容是什么呢?嗯,fibs
是我们已经拥有的列表,tail fibs
在我们到目前为止的列表上调用了tail
函数,该函数返回从第二个元素开始的列表(有点像在Python中说fibs[1:]
)。所以我们将这两个列表- fibs
和tail fibs
- 合并在一起,使用+
函数(运算符)将它们进行压缩 - 也就是说,我们对应位置的元素相加。看一下:
fibs = 0 : 1 : ...
tail fibs = 1 : ...
zip result = 1 : ...
接下来我们要处理的元素是1!但是我们将它添加回我们的fibs
列表中,看看我们得到了什么:
fibs = 0 : 1 : 1 : ...
tail fibs = 1 : 1 : ...
zip result = 1 : 2 : ...
这里涉及到了一个递归列表定义。随着我们使用zipWith (+) fibs (tail fibs)
的方法向fibs
的末尾添加更多元素,添加元素时可用的元素就变得更多了。请注意,Haskell默认是惰性求值的,因此仅生成这样一个无限列表不会导致任何崩溃(只是不要尝试打印它)。
因此,虽然在理论上这与我们之前的数学定义可能相同,但它将结果保存在我们的fibs
列表中(类似于自动记忆化),并且我们很少会遇到在朴素解决方案中可能遇到的问题。为完整起见,让我们采用新的fibs
列表定义我们的fib
函数:
fib n = fibs !! n
如果你还没放弃阅读,说明你已经理解了Clojure代码。看一下这段代码:
(def fib-seq (lazy-cat [0 1]
(map + fib-seq (rest fib-seq))))
我们创建一个名为
fib-seq
的列表。它以两个元素
[0 1]
开头,就像我们在Haskell示例中所做的那样。我们使用
(map + fib-seq (rest fib-seq))
进行惰性连接这两个初始元素 - 假设
rest
与Haskell中的
tail
执行相同的操作,我们只是将列表与其自身组合在较低的偏移处,然后使用
+
运算符/函数组合这两个列表。
经过几次思考和探索其他示例后,生成斐波那契数列的这种方法至少变得半直观了。对于我来说,它足够直观,可以在我不了解的语言中发现它。