函数式编程与声明式语⾔
编程语⾔可以分成两类:
命令式
声明式
事实上,凡是⾮命令式的编程都可归为声明式编程。因此,命令式、函数式和逻辑式是最核⼼的三种范式。为清楚起见,我们⽤⼀幅图来表⽰它们之间的关系。
与命令式编程相对的声明式编程(declarative programming)。顾名思义,声明式编程由若⼲规范(specification)的声明组成的,即⼀系列陈述句:‘已知这,求解那’,强调‘做什么’⽽⾮‘怎么做’。声明式编程是⼈脑思维⽅式的抽象,即利⽤数理逻辑或既定规范对已知条件进⾏推理或运算。
声明式编程的发源
声明式编程发轫于⼈⼯智能的研究,主要包括函数式编程(functional programming,简称FP)和逻辑式编程(logic programming,简称LP)。其中,函数式编程将计算描述为数学函数的求值,⽽逻辑式编程通过提供⼀系列事实和规则来推导或论证结论。
其实⽀持它们的语⾔出现得并不⽐命令式的晚多少——最早的函数式语⾔Lisp(LISt Processor)已有半个世纪的历史,最早之⼀的逻辑式语⾔Prolog(PROgramming in LOGic)也与C同龄。只是由于⼤多数更多地⽤于学术研究⽽⾮商业应⽤,颇有些‘养在深闺⼈未
识’的味道。
起源的不同决定了这两⼤类范式代表着迥然不同的编程理念和风格:命令式编程是⾏动导向(Action-Oriented)的,因⽽算法是显性⽽⽬标是隐性的;声明式编程是⽬标驱动(Goal-Driven)的,因⽽⽬标是显性⽽算法是隐性的。为便于说明,我们分别⽤三种代表性的语⾔来实现阶乘(factorial)运算。
阶乘的三种编程实现
C(命令式)——
1int factorial(int n)
2{
3    int f = 1;
4    for(; n > 0; --n) f *= n;
5    return f;
6}
Lisp(函数式)——
1(defun factorial(n)
1(defun factorial(n)
2  (if(= n 0)
3      1                              //  若n等于0,则n!等于1
4    (* n (factorial(- n 1)))))      //  否则n!等于n* (n-1)
Prolog(逻辑式)——
1// 0! 等于1
2factorial(0,1).
3// 若M等于N-1且 M!等于Fm且F等于N*Fm,则N! 等于F
4factorial(N,F) :-  M is N-1, factorial(M,Fm), F is N * Fm.
以上三段代码区别在哪⾥?C明确给出了阶乘的迭代算法,⽽Lisp仅描述了阶乘的递归定义,Prolog则陈述了两个关于阶乘的断⾔。
声明式编程的本质
我们最早接触的变量是代数⽅程中的x、y、z等,本质上是抽象化的符号,变量值是该符号在给定约束条件下的允许值。⽽命令式编程中的变量本质上是抽象化的内存,变量值是该内存的储存内容。通俗地说,前者好⽐姓名,所指之⼈是固定的;后者好⽐住址,所住之⼈是变化的。此外,等号在代数中是⼀种约束,⽽在许多命令式语⾔中则表⽰赋值。因此 i = i + 1 可以在命令式编程中出现,但绝不可能在数学推理中出现 —— 除⾮在反证法中。
声明式编程让我们重回数学思维:函数式编程类似代数中的表达式变换和计算,逻辑式编程则类似数理逻辑推理。其中的变量也如数学中的⼀样,是抽象符号⽽⾮内存地址,因此没有赋值运算,不会产⽣变量被改写的副作⽤(side-effect),也不存在内存分配和释放的问题。这既简化了代码,也减少了调试——不妨想⼀想,有多少bug是由于某个变量被意外改写或内存管理不慎⽽造成的?
声明式语⾔与命令式语⾔的相通之处
⾸先,所有⾼级语⾔都建⽴于低级语⾔之上,最终转化为机器语⾔,声明式语⾔也不例外。
其次,声明式语⾔与命令式语⾔并⾮泾渭分明,⽽是互相交叉渗透的。⼀些‘⾮纯粹’ 的声明式语⾔也提供变量赋值和流程控制,⽽⼀些命令式语⾔也在逐渐发展,通过利⽤其他程序或增加新的语⾔特征来实现声明式编程。
总的说来,在命令式语⾔中融⼊声明式的元素应当是⼀种趋势。尤其是函数式,它的⼀些特征已经在许多命令式语⾔中得到了⽀持。⽐较⽽⾔,声明式编程重⽬标、轻过程,专注问题的分析和表达⽽不致陷⼊算法的迷宫,其代码也更加简洁清晰、易于修改和维护。从这种意义上说,声明式语⾔天然地就⽐命令式语⾔更⾼级。
既然声明式编程有这么多好处,为什么命令式语⾔不仅占⼤多数,⽽且流⾏程度也不减呢?
目前流行的编程语言编程语⾔的流⾏程度与其擅长的领域关系密切。声明式语⾔——尤其是函数式语⾔和逻辑式语⾔——擅长基于数理逻辑的应⽤,如⼈⼯智能、符号处理、数据库、编译器等,对基于业务逻辑的、尤其是交互式或事件驱动型的应⽤就不那么得⼼应⼿了。⽽⼤多数软件是⾯向⽤户的,交互性强、多为事件驱动、业务逻辑千差万别,显然命令式语⾔在此更有⽤武之地。
值得指出的是,声明式编程并不仅仅局限于函数式和逻辑式。⽐⽅说,C#中的attribute、Java中的annotation和XDoclet库等采⽤的也是具有声明式特征的属性导向式编程(Attribute-Oriented Programming,简称@OP)。再⽐如,Prograph、SISAL等数据流语⾔(dataflow language)采⽤的数据流式编程(Dataflow Programming)与函数式编程有不少共同点,同样属于声明式的范畴。还有⼀些语⾔如Oz、CHIP等⽀持与逻辑式编程相交的约束式编程(Constraint Programming)。此外,⼤家熟悉的数据库语⾔SQL,样式语⾔XSLT、CSS,标记语⾔HTML、XML、SVG,规范语⾔IDL(Interface Description Language)等等都是声明式的。算上它们,声明式语⾔所占的⽐例也是⾮常可观的。此前之所以没有提及,⼀⽅⾯,不少声明式语⾔采⽤的范式并没有专门的名称;另⼀⽅⾯,这些语⾔⼤多是领域特定语⾔,并且不少并⾮图灵完备的,有的连运算都没有。毕竟,⽬前我们的重点还是放在通⽤编程语⾔上。
其实⽤Lisp实现阶乘的⽅法也可以⽤在C上:
1int factorial(int n)
2{
3    return n == 0 ? 1 : n * factorial(n - 1);
4}
这是C的递归实现。除了细微的语法差别外,⼆者的确很相似,这说明⽤命令式语⾔也可以讲出声明式的味道。实际上,命令式语⾔提倡迭代⽽不⿎励递归,早期的Fortran 甚⾄都不⽀持递归。⼀则迭代⽐递归更符合命令式的思维模式,因为前者贴近机器语⾔⽽后者贴近数学语⾔;⼆则除尾递归(tail recursion)外,⼀般递归⽐迭代的开销(overhead)⼤。相反,声明式语⾔提倡递归⽽不⽀持迭代。就语法⽽⾔,它不允许迭代中的循环变量;就视⾓⽽⾔,迭代着眼微观过程⽽递归着眼宏观规律。
具体可以看看这个:漫谈递归
归根结底,编程是寻求⼀种机制,将指定的输⼊转化为指定的输出。三种范式对此提供了截然不同的解决⽅案:
命令式把程序看作⼀个⾃动机,输⼊是初始状态,输出是最终状态,编程就是设计⼀系列指令,通过⾃动机执⾏以完成状态转变;
函数式把程序看作⼀个数学函数,输⼊是⾃变量,输出是因变量,编程就是设计⼀系列函数,通过表达式变换以完成计算;
逻辑式把程序看作⼀个逻辑证明,输⼊是题设,输出是结论,编程就是设计⼀系列命题,通过逻辑推理以完成证明。
绘成表格如下:
范式程序输⼊输出程序设计程序运⾏
命令式⾃动机初始状态最终状态设计指令命令执⾏
函数式数学函数⾃变量因变量设计函数表达式变换
逻辑式逻辑证明题设结论设计命题逻辑推理

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。