Haskell 学习笔记-06:矩阵问题——函数式编程真不容易

函数式编程语言没有循环命令,因此,需要用循环解决的问题一般都用递归代替。但是,很多问题的确不适合递归,比如矩阵的相关操作。我打算探讨一下矩阵有关的算法,看看如何处理这类问题。

练习1 在 N 阶方阵中填写自然数序列,例如:

( 1 2 3 4 5 6 7 8 9 )

代码如下,用两次列表推导式代替双重循环,实现还算简单:

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]result
    t = 1
    for (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]result
    t = 1
    for (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 result
            else 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 result
            else result ++ [[t+1..t+n]] ++ (mat n [] (t+n))
    in  mat n [] 0

尝试各种不同的实现方法,反复比较与过程式编程语言的差异,才能对函数式编程的理解逐步深入。

猜你喜欢

转载自blog.csdn.net/quicmous/article/details/80701167