Why Functional Programming Matters ZZ

发信人: bitapf (北京理工编程爱好者协会), 信区: FuncProgram
标  题: Why Functional Programming Matters
发信站: BBS 水木清华站 (Sun Nov 23 15:11:17 2003)

后面的没翻译完,原文的名字就是这个,大家自己搜一下吧,ps格式的不好给。
想要继续我的翻译的我给你投递原文,或者觉得我翻译得太烂(的确很烂,汗)
,也给你投递。留个mail就行。虽然此文已经是将近20年前的东西了,回顾一下
也是好的。

Why Functional Programming Matters

摘要
随着软件变得越来越复杂,使它结构良好变得越来越重要,结构良好的软件易写易调试,
并提供了许多可以降低未来开发代价的可重用模块。传统的语言在把问题模块化的道路上
有概念上的限制,functional语言把这些限制给去掉了。在本文中,我们特别地展示了fu
nctional语言的两个能够极大推动模块化的特色,high-order函数与lazy evaluation。作
为例子,我们操纵list和tree,编写几个数值算法,并实现alphabeta heuristic(一个用
于游戏程序中的人工智能算法)。因为模块化是成功编程的关键,functional语言对real
 world很有意义。

1 导论
本文尝试向“real world”展示Functional Programming重要性,并通过讲清楚它的优点
帮助functional程序员最大限度地利用这些好处。

Functional Programming之所以称之为Functional Programming是因为程序完全由函数组
成。主程序本身被写成以参数形式接收程序输入和结果为产生程序输出的函数。典型的主
函数是由其他函数定义的,而这些函数反过头来又是由其他更多的函数定义的,直到语言
内建的那些函数为止。这些函数与数学中的函数很像,而且在本文中将由普通的等式定义
。我们的记号遵循Turner的Miranda语言的语法,但应该能够在事先不了解functional语言
的前提下读懂。(Miranda式Research软件有限公司的商标)

Functional Programming的特质与优点经常或多或少地归结于一下几条。Functional程序
不包含赋值语句,因而也没有变量,一旦给了一个值永远不能改变。更一般地说,functi
onal程序一点side-effect都没有,一个函数调用没有计算结果之外的影响。这消除了bug
的一个主要来源,并使得执行的顺序不是彼此相关的——因为没有改变表达式的side-eff
ect,它就能在任何时刻被求值。这把程序员从规定程序流程的重担中释放出来,因为表达
式可以在任何时候被求值,可以自由地用变量的值代换变量,反之亦然——也就是说程序
是“referentially透明的"。这种自由使得functional程序在数学意义上比传统程序更易
驾驭。

对好处的这么一个归纳总体上来说是不错的,但不要对外界对它不以为意感到奇怪。它说
得更多的是functional programming不是什么(它没有赋值,没有side-effect,没有流程
控制),而不是它是什么。functional程序员听起来很像中世纪的僧侣,抛弃自己生活中
的乐趣以获得使自己变成圣德的希望。对于哪些更关心实实在在的利益的人,这些好处不
是十分地有说服力。

functional程序员争辩地说确实有很多实在的好处——因为functional程序非常精简,一
个functional程序员比传统的程序员有大得多的生产力。而这是为什么呢?唯一的似是而
非的理由暗示了这些“优点”的基础是传统程序包含的90%代码是赋值语句,而在functi
onal程序中这些被省略了!这显而易见是荒唐的。如果仅仅省略赋值语句就可以带来如此
多的好处,那么FORTRAN程序员可能已经享用20余年了。通过省略语言特性(不管他们是多
么糟糕)的办法使得其变得更有威力从逻辑上讲都是不可能的。

甚至一个functional程序员都会被这些所谓的优势感到不满足,因为这无益于他们挖掘fu
nctional语言的威力。一个人不能写尤其特别地缺少赋值语句或者特别地referentially透
明的程序。这儿没有一个衡量软件质量的准绳,因而没有要努力达到的目标。

显然这样的对functional programming的描述是不恰当的。我们必须找出一些到位的东西
——一些不仅仅解释了functional programming威力而且该处了一个functional 程序员应
该朝之努力的明确指引。

2 同结构化编程的类比
在functional和structured programming之间作一个类比是有好处的。在过去,结构化编
程的好处或多或少的总结出这么几条。结构化编程没有goto语句,在结构化程序中的代码
块没有多个入口或者出口。结构化的比非结构化的同样的程序要在数学意义上更易处理。
结构化编程的这些“优点”和我们前面讲的functional programming的“优点”的精神上
是很相似的。他们本质上是negative statements,并导致了许多没有结果的关于“重要的
goto”的争论,等等。

结构化程序的这些好处是清楚的,如后见之明一样虽然有帮助,但并没有切入问题的核心
。结构化和非结构化程序的最重要的区别是结构化程序以模块化的方式设计。模块化设计
给它带来了生产力上的很大提高。首先,消息的模块可以更快更容易地被编写。其次,通
用的模块能够被重用,导致了后来的程序的更快速的开发。第三,程序的模块可以独立地
被测试,有助于减少花费在调试上的时间。

goto的滥用,等等,对这些问题来说无关紧要。他对“小软件的编程”有帮助,但是模块
化设计对“大尺度的编程”有帮助。因而可以在FORTRAN或者汇编语言中享受结构化编程的
好处,甚至要作多一些的工作也无所谓。

大家基本上已经接收了模块化设计是成功编程的关键,而且像Modula-II[Wir82], Ada[oD
80] 和 Standard ML [MTH90]这些语言中特别包含了改善模块性的设计。然而,有一点很
关键的地方经常被忽略。在编写模块化程序来解决问题的时候,首先要把问题划分为子问
题,然后逐个解决小问题,最后把这些解决方案粘合起来。分解原有问题的手段直接依赖
于最终粘合解决方案的办法。因而,要从概念上增强模块化的能力,必在编程语言中提供
新的粘合剂。复杂的作用域规则和提供单独编译只能帮助具体的细节;他们没有提供新的
分解问题的概念上的工具。

你可以从木匠的比喻中认识到粘合剂的重要性。一个椅子能够通过用正确方法把位子,腿
,靠背这些部分粘合起来而很容易的制造出来。但这依赖于接头和木胶的作用。缺少了这
些东西,作一个椅子的唯一办法就是从一块硬实的木头中刻出来,一件困难得多的任务。
这个例子显示了模块化的巨大威力和拥有合适粘合剂的重要性。

现在让我们回到functional programming来。我们在本文的余下部分讨论functional语言
提供的两种新的,非常重要的粘合剂。我将提供许多可以用新的手段,因而加大地被简化
乐得,模块化的例子。这是functional programming的关键能力,它为被极大改进的模块
化设计提供了可能。这也是functional程序员必须朝之努力的目标——更小更简单更通用
的模块,通过我们将要描述的新的粘合剂粘合起来。

3 把函数粘合起来
这两种新的粘合剂的第一个使得简单的函数可以粘合在一起成为更复杂的东西。这由简单
的list处理问题演示——把list中的元素加起来。我们这样定义list

listof X ::= nil --- cons X (listof X)

这意味着X(不管X是什么)的list要么是nil,代表没有元素的list,要么是一个X和另一
个X的list的cons。一个cons代表一个list,它的第一个元素是X,而接下来的元素是另一
个X的list的元素。这儿的X可能代表任何一种型别——例如,如果X是“整数”,那么定义
说的是整数的list要么是空的要么是一个整数要么是整数和另一个整数的list的cons。遵
循一般的实践经验,我们将简单地通过把它们的元素用方括号扩住的办法写下list,而不
是显式地写出cons和nil。这只是符号习惯上的速记。例如,

[] means nil
[1] means cons 1 nil
[1,2,3] means cons 1 (cons 2 (cons 3 nil))

因为list的元素可以通过循环调用函数sum加起来。sum必须为两种参数做出定义:空的li
st(nil),和一个cons。因为0个数的和是0,我们定义

sum nil = 0

而且因为一个cons的和可以通过计算list的第一个元素和其他元素的和的和得出,我们定


sum (cons num list) = num + sum list

检查这个定义,我们可以看到只有下面加框的部分是特定于计算和的。

          +---+
sum nil = | 0 |
          +---+
                          +---+
sum (cons num list) = num | + | sum list
                          +---+

这意味着和的计算可以通过把通用的递归模式粘合加框的部分被模块化。这个递归模式习
惯上称为reduce,所以sum可以这样表示

sum = reduce add 0

其中为了方便,传递给reduce的是带两个参数的函数而不是一个操作符。Add被如下定义


add x y = x + y

reduce的递归定义可以通过参数化sum的定义被继承,例如

(reduce f x) nil = x
(reduce f x) (cons a l) = f a ((reduce f x) l)

这儿我们在(reduce f x)外加上括号是像让它代替sum的意思清楚一些。习惯上,这个括号
是被省略的,因而((reduce f x) l)被写作(reduce f x l)。一个像reduce这样传递3个参
数但只应用2个的函数,被认为是剩下的一个参数的函数。一般的说,一个传递了n个参数
但只应用m(

通过这样模块化sum,我们可以重用这些部分获得好处。最有意思的是reduce,它能够让不
用再编程就能写出把list中的元素乘起来的函数:

product = reduce multiply 1

它还能被用于测试是否布尔值的list是否为真

anytrue = reduce or false

或者它们是否都是真

 
alltrue = reduce and true

一个理解(reduce f a)的办法是把它认为是用f替换list中所有cons,用a替换nil的函数。
拿list [1,2,3]作为例子,它的意思是

cons 1 (cons 2 (cons 3 nil))

然后 (reduce add 0)被转换为

add 1 (add 2 (add 3 0)) = 6

而 (reducee multiply 1) 转换为

multiply 1 (multiply 2 (multiply 3 1)) = 6

所以明显的, (reduce cons nil) 仅仅把一个list复制了一遍。因为一个list能够通过c
ons另外一个list的元素于其前面被附加到它的后面。我们发现

append a b = reduce cons b a

例如,

 
append [1,2] [3,4] = reduce cons [3,4] [1,2]
                   = (reduce cons [3,4]) (cons 1 (cons 2 nil))
                   = cons 1 (cons 2 [3,4]))
                        (replacing cons by cons and nil by [3,4])
                   = [1,2,3,4]

一个把list中所有元素翻倍的元素能够写成

      doubleall = reduce doubleandcons nil
where doubleandcons num list = cons (2*num) list

Doubleandcons能够进一步的模块化,首先是

      doubleandcons = fandcons double
where double n = 2*n
      fandcons f el list = cons (f el) list

然后由

 
fandcons f = cons . f

其中“.”(composition函数,一个标准操作符)被定义为

(f . g) h = f (g h)

我们可以通过应用一些参数,看到fandcons的新的定义是正确的:

   fandcons f el = (cons . f) el
                 = cons (f el)
so fandcons f el list = cons (f el) list

最终的版本是

 
doubleall = reduce (cons . double) nil

利用我们刚得出的更深入的模块

doubleall = map double
map f = reduce (cons . f) nil

其中map把任意函数f作用于list的所有元素。map是另一个通常很有用的函数。

我们甚至可以写出一个求由list的list代表的矩阵所有元素和的函数,它是

summatrix = sum . map sum

map sum使用sum来相加所有的行,然后最左边的元素加到行的总和来获得整个矩阵的和。


这些例子应当足以使读者信服一个小小的模块化设计能够起到很大作用。通过模块化简单
函数(sum)为“higher order函数”和一些简单参数的组合,我们得出了一个能够用于不
用更多编程努力就能写出很多其他操作list的函数的部件(reduce)。我们没必要只停留
在关于list的函数上。另一个例子,考虑有序标记的tree这种数据结构,定义为

treeof X ::= node X (listof (treeof X))

这个定义告诉X的tree是一个有一个标签X的节点,以及一个subtree的list,subtree又是
其它X的tree。例如,这个tree

    1 o
     / /
    /   /
   /     /
2 o       o 3
          |
          |
          |
          o 4

将由这个代表

node 1
    (cons (node 2 nil)
          (cons (node 3
                     (cons (node 4 nil) nil))
               nil))

不再考虑一个例子然后从中抽象一个higher order函数,我们直接得出一个类似reduce的
redtree函数。回忆reduce带两个参数,一个代替cons,一个代替nil。因为trree是用节点
,cons和nil构建的,redtree必须有代替这些东西的参数。既然tree和list是不同的类型
,我们必须定义两个函数,分别操作自己的类型。所以我们定义

 
redtree f g a (node label subtrees) =
        f label (redtree' f g a subtrees)
redtree' f g a (cons subtree rest) =
        g (redtree f g a subtree) (redtree' f g a rest)
redtree' f g a nil = a

通过粘合redtree和其他函数能够定义出许多有意思的函数。例如,所有数中的数字标签能
够通过这个加起来

sumtree = redtree add add 0

拿前面我们写下的tree作为例子,sumtree给出

 
add 1
   (add (add 2 0)
        (add (add 3
                 (add (add 4 0) 0))
            0))
= 10

tree中所有label的list能够用这个计算

labels = redtree cons append nil

同样的例子给出

cons 1
    (append (cons 2 nil)
            (append (cons 3
                          (append (cons 4 nil) nil))
                    nil))
= [1,2,3,4]

最终,我们可以定义一个类似于map的函数来把函数f应用于tree中的所有label:

maptree f = redtree (node . f) cons nil

所有这些能够做到,是因为functional语言允许在传统编程语言中不可再分的函数用部件
的组合的形式表示——higher order函数和一些特制的函数。一旦定义了,这样的higher
 order函数使得许多操作能够很容易的编制出来。无论何时定义了一个新的数据类型,就
应当写higher order函数来处理它。这使得容易操作数据类型,而且隐藏了它的实现细节
。和传统编程的最好类比是可扩展语言——它是一种能够在需要的任何时候用新的控制结
构扩展的编程语言。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值