函数式编程语言没有循环命令,因此,需要用循环解决的问题一般都用递归代替。但是,很多问题的确不适合递归,比如矩阵的相关操作。我打算探讨一下矩阵有关的算法,看看如何处理这类问题。
练习1 在 N 阶方阵中填写自然数序列,例如:
代码如下,用两次列表推导式代替双重循环,实现还算简单:
mat123 n = let row123 n k = [x|x <- [k.. k+n-1]]in [row123 n k| k <- [1,1+n..n*n-n+1]]
写完把这段代码,我觉得和 C 语言比还是有些不舒服,所以停下来仔细比较一下二者的差别,然后再看看是否有更好的实现方法。C 语言(伪代码,不想在代码上浪费时间了)代码如下:
int[][] mat123(int n){int [n][n]resultt = 1for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j) {result[i][j] = t;++t;}}return result
}
和前面的 Haskell 代码相比,实际上变量 t 扮演了全局变量的角色,生命周期存活在两个循环之外。C 语言代码行数较多,但是计算过程比较容易理解,就是一篇通俗小说。Haskell 程序虽短,但理解难度较大,设计难度也很大。所以,不能单纯认为函数式编程代码简短,工作量就会很少,这是不对的。一首简短的诗词需要花费的功夫,未必小于一篇通俗小说。
函数式编程如果也能实现一个和 C 语言风格相近的设计, 有利于两种语言思维的转换,也有利于理解两种编程模型内在的区别。另外,我不太认同那种一味追求最短的代码的做法,代码易读性应该是第一位的。
先来分析一下问题的难点。首先把 C 代码简化如下:
int[][] mat123(int n){int [n][n]resultt = 1for (int i = 0; i < n; ++i){result[i] = row()++t}return result
这个 row() 函数就是原来的 for (int j = 0;…) 那个循环,它本质上有三个全局变量,n,t 和 i。因此,for (int i…)这个循环每一轮调用 row() ,其返回结果都不一样。这一特征是典型的面向过程语言的“副作用”,函数式编程是不允许这种函数存在的,这就造成在 Haskell 中复现这个思路困难重重。下面考虑用 Haskell 模拟这一思路实现算法,代码如下。
mat123' n =let mat n result t = if t == [] then resultelse result ++ [take n t] ++ mat n [] (drop n t)in mat n [] [1..n*n]
定义了一个临时函数 mat ,把 C 语言中的全局变量result、t 作为参数传递,每次递归调用,利用参数传送修改它们的值。就这个例子而言,找到了处理全局变量的方法,但是感觉这一段代码写的好垃圾哦。
又改进了一下,占用资源少了点:
mat123'' n =let mat n result t = if t >= n*n then resultelse result ++ [[t+1..t+n]] ++ (mat n [] (t+n))in mat n [] 0
尝试各种不同的实现方法,反复比较与过程式编程语言的差异,才能对函数式编程的理解逐步深入。