Dopcn's Blog

life and work

函数式与过程式编程区别之赋值行为

Posted at — Nov 18, 2015

对于常见的过程式编程语言赋值行为和定义变量是同一种形式,都是用等号进行。对于函数式编程语言来说定义和赋值有着显著区别。

由于过程式编程更”容易”进行赋值所以经常采用在整个作用过程中使用起记录作用的状态变量,用一个获取 Fibonacci 前N 项中偶数的函数做示例:

func evenFibs1(n: Int) -> Array<Int> {
    var result = Array<Int>()
    var x = 0, y = 1, count = n
    result.append(0)
    while count > 2 {
        (x, y) = (y, x+y)
        if y%2 == 0 { result.append(y) }
        count--
    }
    return result
}

func evenFibs2(n: Int) -> Array<Int> {
    func fib(n: Int) -> Int {
        var tmpArray = [0, 1]
        func fib2Inside(m: Int) -> Int {
            if m >= tmpArray.count {
                tmpArray.append(fib2Inside(m-1) + fib2Inside(m-2))
            }
            return tmpArray[m]
        }
        return fib2Inside(n)
    }
    func isEven(n: Int) -> Bool {
        if n%2 == 0 { return true }
        else { return false }
    }
    return Array<Int>(0..<n).map(fib).filter(isEven)
}

方法一中使用一个局部变量作为盛放结果的容器,不断将这一变量赋值为新的值,在方法的最后返回这个值,而方法二则是直接作用于一个数组得到结果。

这两种方法对比,显然方法一的效率更好一些,方法二是在模块化的方面更显著一些,模块化意味着这个方法很容易抽象为一个对数组每个值作用然后过滤的函数,作用函数和过滤函数都可以轻易的更换,而方法一的变更就没有那么容易了。事实上方法二里面的 fib 函数为了求值效率是按照方法一的思路实现的。

因为使用赋值带来的另一个区别是,过程式编程语言中不同语句之间的顺序对于程序正确运行来说很重要,对比求阶乘的两种写法:

func factorial1(n: Int) -> Int {
    var product = 1, counter = 1
    while counter <= n {
        product = counter*product
        counter+=1
    }
    return product
}

func factorial2(n: Int) -> Int {
    func iter(product product: Int, counter: Int) -> Int {
        if counter > n {
            return product
        } else {
            return iter(product: counter*product, counter: counter+1)
        }
    }
    return iter(product: 1, counter: 1)
}

方法一中while循环里的语句是绝对不能调换位置的。说明这个问题的一个更好的例子是,下面这两种定义 x y 的方法

 y = x * 2
 x = 3

 x = 3
 y = x * 2

在函数式语言中两种定义方法定义出的 x y 值是一样的,而一般的过程式语言则要看第一种定义方法之前 x 的值是什么才能决定 y 的值。