浅析函数式编程与命令式编程的区别(三)风格的区别
程序设计语言有很多种风格。传统的命令式语言比如:Fortran C和Pascal都是面向过程的,它们主要的程序设计单元是过程。它们采用一种自顶向下的编程风格:一个程序的目的是完成这七件事,那么我就把它分成七个主要的子例程。第一个子例程要做这四件事,所以它将进一步细分成它自己的四个子例程”,如此这般。这一过程持续到整个程序被细分到合适的粒度每一部分都足够大可以做一些实际的事情,但也足够小到可以作为一个基本单元来理解。
现代的命令式语言比如:C++ Java和C#则是面向对象的,它们将对象作为程序的基本单元,将程序和数据封装其中,通过对象的之间的协作来解决问题。
面向过程和面向对象是命令式语言主要的两种风格,关于这两种风格的介绍已经很多,我就不在此赘述了。下面我们将目光转向函数式编程。
首先我们来看看函数式编程的一些典型特征,其中许多都是命令式语言里面没有的(不过编程语言一直都在进化,而且现在的趋势是命令式语言正在想函数式语言靠拢,因此现代级的语言如C#、Java也具有一些函数式语言的特征)
- 函数作为第一级的值(第一类对象)和高阶函数
- 广泛的多态
- 递归
- 机构性的函数返回
- 递归
- 表类型和表操作
- 结构性对象的构造符(聚集值)
- 废料收集(垃圾回收机制)
下面开始针对以上的特征来介绍函数式语言的程序设计风格
前一篇我提到过函数式程序是跟值打交道的,它们的工具是表达式。表达式主要是由函数调用组成的。在函数式语言里,函数被作为第一类对象处理(第一类对象是指可以作为参数传递、从函数里返回、可以(在有副作用的语言里)赋值给变量的东西,更严格的定义还要求第一类对象能在运行时创建(计算出)一个新值的能力)。比如comman Lisp内置的sort函数就接受一个谓词函数作为参数对列表排序:
CL-USER> (sort '(2 1 3 4) #'<)
(1 2 3 4)
再比如我们可以定义一个返回函数的函数:
CL-USER> (defun make-adder(n)
#'(lambda (x) (+ x n)))
MAKE-ADDER
这个函数接受一个参数n然后返回一个加法函数。这个加法函数将返回参数加上n的值。比如我们可以这样使用它:
CL-USER> (funcall (make-adder 2) 2)
4
高阶函数是一个可以去函数为参数或产生一个函数为结果或者两者皆有的函数。一种常见的类型就是函数复合(和数学的复合函数差不多),它有两个函数参数,并且产生一个函数,这个结果函数的值是将第一个实参数函数应用到第二个函数的结果上产生的。比如:h(x)=f(g(x))。另一个常见的类型是:应用到所有参数。它取一个函数作为参数,将这个函数应用到自变量表中的每一个值。比如Lisp中的mapcar函数:
CL-USER> (mapcar #'(lambda(x)(+ 3 x)) '( 1 2 3))
(4 5 6)
这里mapcar第一个参数就是一个函数(这里是一个匿名函数),第二个参数是一个表,然后mapcar将函数应用到每一个列表元素上。
多态性在函数式语言中极为重要,因为这样能使得函数尽可能广泛的一大类参数上。像Lisp这样的动态语言,内在就是多态的。ML语言有类型系统,但是它的类型系统是多态性的,它忽略掉无关的数据类型,在编译时进行类型推理来支持多态性。
递归可以说是函数式语言用得最多的特性了。在函数式程序中,变量是通过外部传入或者申明获得值的。变量不能被改变,只能通过递归调用可以制造一系列变化的参数值。而且在没有副作用的情况下,是不可能实现循环的,递归成了重复做一件事情的唯一选择。从某种意义上讲,递归的语义比循环很容易理解。比如我们可以定义一个递归计算表长的函数:
CL-USER> (defun my-length(lst)
(if (null lst)
0
(1+ (my-length (cdr lst)))))
MY-LENGTH
表可以说是天生的函数式语言的数据结构,因为它们有自然的递归定义,很容易基于第一个元素和(递归地)对表中其他元素的操作的方式完成各种处理。Lisp语言就将表作为数据和程序的基本结构,实际上Lisp原本的意思就是表处理语言。
最后我们来总结一下到底什么是函数式编程风格:纯函数式编程不使用变量或赋值语句产生结果,它们使用函数的应用、条件表达式和递归来作为执行的控制,并且使用函数形式来构造复杂的函数。这样的好处就是纯函数式编程不会产生副作用,当给予同样的参数时,函数的执行总是会产生同样的结果。这种特征被称为引用透明性。
Lisp最原始版本是纯函数式的,但是后来的方言都加入命令式特性。但是Lisp内置的表处理函数仍然是以函数式的风格编写的,下面我们就用Lisp的表处理函数来进一步说明什么是函数式程序设计。
比如函数reverse就是将返回一个顺序相反的列表,按函数式的风格,reverse是不能改变参数的,只返回一个顺序相反的列表。
CL-USER> (setq lst (list 1 2 3))
(1 2 3)
CL-USER> (setq rlst (reverse lst))
(3 2 1)
CL-USER> lst
(1 2 3)
我们自己也可以来实现一个函数式的reverse函数,请看On Lisp上的例子:
(defun good-reverse (lst)
(labels ((rev (lst acc)
(if (null lst)
acc
(rev (cdr lst) (cons (car lst) acc)))))
(rev lst nil)))
通过这两个例子,我们可以看出函数式编程的本质就在于:函数式编程是通过返回值而不是副作用来编程的。程序完全由无副作用的函数构成,函数完全基于参数来计算结果。
作者曰:函数式编程风格的核心就在于无副作用编程,而函数式语言诸多的特性也都是围绕这一核心设计的。无副作用编程是一种很好的想法,副作用可能使得程序难以阅读、难以编译。无副作用使得表达式具有引用透明性,使之与求值顺序无关。但不幸的是,在现实的程序设计习惯中,最正宗的副作用(赋值)正扮演着核心角色。或许我们已经在命令式编程的路上走得太远,以至于忘记还有函数式编程这么一个选择。
参考文献:程序设计语言-实践之路 Michael Scott
程序设计原理 8th edition Robert W. Sebesta
ML程序设计教程 Lawrence C. Paulson
On Lisp Paul Graham
PS:这是本系列文章的最后一篇,其实关于函数式编程还有很多值得说的东西,我这几篇文章也只是起一个抛砖引玉的作用,我觉得要真正领略一种语言或是一种编程风格,最好的办法就是去学习使用它。最后感谢耐心看完这一系列文章的读者,没有你们的关注,就没有我的坚持。