哪种语言将统治多核时代 再看函数式语言特性

时间:2021-07-12 01:52:40

最近这几年,软件开发语言可谓是层出不穷。在这些新的编程语言中,最多的就是函数式语言。本文将向你介绍函数式语言的概念、术语、方法以及几种典型的函数式语言。本文面向的读者是那些已经懂得其它编程语言、但却对函数式语言没有了解的开发人员。

什么是函数式语言?

如果你已经用面向对象的语言(例如Java和C#)写了很长时间的代码,那么可能很难想象出另一种新的编程思维方式,而函数式语言恰恰做到了这一点。它的核心就是通过对算法进行功能分解,从而解决软件问题。在函数式语言里,函数是首要的。如果你是Java阵营出身,那应该能理解到这之间的差异。在Java中,实现某种方法的唯一形式就是将其作为某个类的成员。

虽然最近风头正劲的某些特殊语言引人注目,但是函数式语言其实也可以代表一门技术,而不仅仅是一种语言。我们可以用函数式编程的方式,用面向对象的编程语言实现一般的功能(后面的章节里我们就会看到一个用函数式方法编写的C#程序)。虽然这可以实现,但是在稍大一点的程序中,我们很快就会感觉到缺乏表现力,反模式(anti-pattern)也随之出现。试想一下,不用extend和implement关键字写个上规模的Java程序有多痛苦。由于这些困难的存在,人们需要一种新的语言:一种函数式语言

为何需要函数式语言?

很明显,现代计算平台上发生了一个重大变化,那就是多核技术的引入。除了上网本和PDA,我们甚至都找不出还采用单核处理器的台式机和笔记本电脑了。我们正在向多核心,多处理器发展,并且所有迹象都表明这一趋势将继续下去。除了采用多核心之外,高运算量和高复杂度的算法应用都倾向于优化使用图形处理单元(GPU),从而提高并行性。归纳起来,从开发者的角度来说,这些都属于并发问题的范畴。

我们大多数的编程语言都不容易实现并发。想想几十年前,C语言程序里的错误处理直接就被代码基底(code base)丢弃了。它与业务逻辑混在了一起。C函数成功后就将返回0 ,失败则返回错误代码。很明显这不是很理想的办法,但是C语言本身的表达能力限制了开发者们用其它方法来进行出错处理。其他语言对此进行了改进,在C++或者Java中,出错时会抛出异常,异常处理程序把出错处理和正常事务处理分离开来。有些人可能会说这也算不上什么太好的办法,但是这至少是个不小的进步了。在解决并发这个问题上,我们所掌握的技术的成熟度也和这差不多。如果想要在一个用面向对象语言编程的程序里实现并行,那编写起来真得费一番脑筋。像生成一个打印任务线程并不需要处理多少并发控制,但是往往它还会牵扯到进程间的状态共享显示器的阻塞。随着内核数的增加,同时运行的线程数可能进一步增大,系统的效率也将随之降低。这时,我们就需要一种新的语言,让我们能从这些细节工作中抽身出来,以更好地利用并发。

函数式语言已经在简化并行开发中证明了它的作用, 这得益于它既不用共享内存,也不会产生副作用(side effect)的函数。进一步深入函数式语言,你就会发现它让开发者从并发这个概念中抽身出来了,让开发者不用老是想着现在CPU是在并发作业。许多语言实现了一种并发开发模式,通常称之为Actor模型。在这种模型下,进程间传递消息而不是共享状态从而消除线程阻塞

函数式语言的另一大宝贵优点就是简洁。在Stuart Halloway的《Programming Clojure》一书第一章中,Stuart展示了3行clojure代码,这比用Jakarta Commons框架开发减小了三分之一的代码量,同时还体现出更清晰明了的逻辑思路。

有一个很重要的观点就是,函数式语言不是用来取代面向过程或者面向对象的编程语言的。看看我们在上一节中所列举出来的几种函数式语言,就会发现许多新的函数式语言都是多范型(multi-paradigm),很多时候这些语言都是运行在虚拟机上,并且作为其它面向对象语言和命令式语言的桥梁出现的。选择适合手头工作的语言才是最重要的。我希望从业者们在开发主流应用时继续使用Java,Groovy或者C#这些通用语言,但当面临着一个极为复杂的算法或需要实现高并发时,最好还是转而用函数式语言来集成这些方案。这也正是Neal Ford说了多年的 “多语言程序员”(polyglot programmer)。对于此类程序员的形成,我们可以参考一下一位Java兼Scala开发者的学习历程

几种典型的函数式语言

当我们回顾历史上的编程语言时,就会发现其实函数式语言并不是一个新生事物,它早就出现过。其中最广为人知的几种“祖父”级语言包括:LISP和FORTRAN 。自1980年代中期以来,这些语言在企业和商业开发领域逐渐让位于面向对象的开发语言,流行领域也逐渐缩小到只剩下学术界。不过下面列出的这几种函数式语言最近正在向商业领域发起反攻

◆Erlang:这是一种以A.K Erlang的名字命名的通用并行编程语言。它有函数式语言的元素,以及一个Actor 并发模型,从而简化并行开发工作。编辑推荐对Erlang感兴趣的读者阅读一下51CTO以前的一次访谈:因并发而生 因云计算而热:Erlang专家访谈实录

◆Haskell:这是一门已经有超过20年历史的开源编程语言,它的设计宗旨就是成为一门纯粹的函数式语言。

◆OCaml:面向对象的Caml(Objective Caml)是Caml语言的一个开源版本,Caml语言可以算是ML语言的一个方言版了,ML语言1970年就已经开发出来了,也是作为一种通用函数式语言存在的。它被认为是后来出现的F#等多种函数式语言的基础。

◆Lisp:表处理语言(List Processing Language)是一种函数式语言,最初是于1958年拟定的。由它派生出了许多分支。

◆Scala:Scala 语言的设计目标是在Java虚拟机上实现函数式和面向对象这两类编程语言的集成。它是一种强类型的编程语言。Scala编程语言近年来的流行度在不断提升,编辑推荐读者参阅51CTO的Scala编程语言专题

◆Clojure:Clojure是Lisp语言的一个现代分支,它运行在Java虚拟机上,是为并发程序开发设计的。它是一种动态类型编程语言。

◆F#:这是一种运行在.Net CLR平台上的新语言。它是OCaml的一个分支,它兼具了函数式和命令式面向对象语言的特点。同时它也是一种强类型的编程语言。F#在未来的.NET平台上有重要的作用,将在Visual Studio 2010中被正式包含

值得注意的是函数式语言并不一定要是动态语言(dynamic language)。函数式语言允许动态或静态类型。这里所列出的语言只是各种各样函数式语言中的一个子集,每一种实现了某种特定的需求。本文将介绍好几种典型函数式语言,而不是专门讲解某一种语言。另外我们还有一个没有回答的问题就是:为什么现在对函数式语言的需求越来越强烈?

 

 

函数式语言的特色

函数式语言的根本宗旨之一就是它不是一种命令式(imperative)语言。在命令式语言中,函数中定义的变量通常都代表内存中的一块特定大小的区域,而且赋给它的值也通常允许在整个方法中改变。而在函数式语言中,对变量的赋值是绑定性的,就像在数学函数式中那样。比如说,有这样的数学式:let x=2。这就是说对于这个问题,x的值为2。x的值不能改变,总是2。按照这样的模式,我们平常编程过程中的一些写法就没有意义了,例如这个赋值语句:let x=x+1。在命令式语言中这是有意义的,但是在数学上,这是没有任何意义的,因为x=x+1是无解的。理解这个概念后,函数式开发(functional development)就算是上路了。

和在数学中一样,函数式语言中的赋值并不仅限于数值型。方法在函数式语言中是最为重要的。因此,一个方法闭包(method closure)可以用来给变量赋值,并且被传递或调整到其它的函数表达式里。用数学表达式来说就相当于:let x=f(y)。数学上称之为:x是以y为自变量的函数f 的函数值。任给一个y值,都有一个对应的x值。这就是函数式编程的另一个核心思想。只要y不变,那么x也总是一个特定的对应值,不会发生改变。

虽然不同的函数式语言之间有一些不同之处,但是他们都有以下一些共同点:

◆函数闭包支持

◆高阶函数

◆用for流程来实现递归

◆没有副作用(side-effects)

◆把重点放在“要计算什么”,而不是“如何去计算”上。

◆引用透明性(Referential transparency)

函数式语言的功能和术语

伴随着函数式语言的发展,涌现了许多新的术语,但是没有哪种能比Lambda产生得更快。就像我们前面提到的一样,函数式编程和数学界有很大的联系。Lambda指的是λ演算(lambda calculus 或 l-calculus)λ演算是一套用于研究函数定义、函数应用和函数递归的系统。

哪种语言将统治多核时代 再看函数式语言特性 

清单1 :一个简单的Lambda表达式

还有许多的λ表达式我们这里都不再深入讨论了。如清单1所示的几个简单表达式,λ提供了一种全新的语法。上面所举的例子表示的是一个一元函数,这意味着函数只需要一个参数,或者说元数是1.在清单2中,我们可以看到把一个函数作为另一个函数的参数。

哪种语言将统治多核时代 再看函数式语言特性 

清单2 :一个简单的λ函数作参数传递

λ表达式在线指引上对这些有详细解释。在清单2中,每行表达式都是等价的。x的函数被当作参数传递给函数f,并且作用在3上。函数x作用在3上就得到了3+2。在函数式语言里,把一个函数作用在另一个函数上是非常常见的一种做法。下面我们考虑清单3:

哪种语言将统治多核时代 再看函数式语言特性 

清单3 :用函数赋值

在清单3中,有3个函数。函数scale_by_2是以scale函数为参数并且作用在2上。它的返回值就相当于 λ n.x * 2 。这个表达式。函数式开发通常就是一层一层地组建这种类型的函数。

 

 

 

闭包(Closure)

函数式语言的另一个重要术语和关注点就是闭包。闭包在现在的各种编程语言中都很常见,这个术语常用来表示一个方法引用(method reference)或者一个匿名函数(anonymous function)。技术上看,闭包就是动态分配的一个含有代码指针(code pointer)的数据结构,这个代码指针指向一个计算函数结果的代码片段以及一个受限变量(found variable)环境。闭包用来把一个函数和“私有”变量联系起来。许多语言里的匿名函数就是用来实现这一目的的,这也常常是让初学者看不懂的地方。

 
  1. Function powerFunctionFactory(int power) {  
  2.    int powerFunction(int base) {  
  3.        return pow(base, power);  
  4.    }  
  5.    return powerFunction;  
  6. }  
  7. Function square = powerFunctionFactory (2);  
  8. square(3); // returns 9  
  9. Function cube = powerFunctionFactory (3);  
  10. cube(3); // returns 27 

在清单4里,factory这个函数返回的是一个求幂次的函数。当我们调用square函数时,它所需要的power这个变量根本就不在作用域内,为什么这样也有意义呢?powerFunctionFactory这个函数返回后按理来说它的堆栈应该也就随之释放了。cube函数也有相同的问题,只不过它求的幂次不一样。要实现这样的语法要求这种语言必须保存变量值,并且要为所创建的每个函数保存变量值。这就称为闭包。

闭包允许把自定义的行为作为函数参数传递,这就引出了另一个重要的术语,“柯里化”(currying)。

柯里化(Currying)

柯里化这个名字听起来很深奥,实际上它指的就是把一个多参数函数转换成只需要单个参数的函数链的这种变换。因此,考虑一个函数 foo(x, y) 它的结果是 z 的值,或者我们把它写成 foo(x, y) -> z 。现在,我们得把它分解成多个函数,每个函数都需要一个函数作为传入参数或者返回值。看出来这种技术与λ演算之间的关系了吗?

如果有 bar(x)->baz, baz(y)->z。这表示bar函数将以x为参数并且返回函数baz。然后当baz以y为参数时,它的结果就是z。因此,foo(x, y) -> z可以用如下方式表示:

bar(x) -> baz

baz(y) -> z

还是让我们结合一个C#实例看看吧。下面这段代码在C# 3.5里是正确的:

 
  1. Func< int, Func< int,int>> scale =  
  2.    x => y => x * y;  
  3.  
  4. var scaleBy2 = scale(2);  
  5. scaleBy2 (100); 

按正常做法,我们应该编写一个方法,以数值100和倍数2为参数。但是,采用函数式编程的方法,函数scale(2)返回一个可以用来给变量赋值的函数。我们把那个返回的函数称为scaleBy2,当然这很容易“链式”地进行下去。通过对一个函数引用进行命名,我们就有了一个可以被调整至整个程序里面使用的函数了。如果你没有搞清楚这些,没有关系,我们下面将继续探讨函数式编程的基础。

数据结构

函数式语言中的数据结构包括元组(tuples)和单体(monad)。元组是不可改变的对象序列。序列,链表和树也是函数式语言中非常常见的数据结构。大部分的语言都提供对这些数据结构的运算符和库,以简化对它们的运算。

一个单体是一个用来反映控制流程或者运算的抽象数据类型。引入它的目的是为了避免使用可能带来副作用(side effect)的语法来表达输入输出操作和状态变化。

模式匹配(Pattern Matching)

模式匹配并不是函数式编程的创新,也不是专用于函数式编程。但是它和函数式语言有广泛联系,因为其它主流编程语言大都没有这一特性。模式匹配说白了就是一种对值或者类型进行匹配的简洁方法。如果你曾经写过很复杂的if,if/else,或者switch语句,那么你应该已经能认识到模式识别的价值了。清单6是一个用Mathematica编写的匹配程序,用于求一个斐波纳契序列(Fibonacci sequence)。

 
  1. fib[0|1]:=1   
  2. fib[n_]:fib[n-1] + fib[n-2] 

清单6 :用Mathematica写的模式匹配范例

对0或1进行匹配的结果是1.对其它任何数的匹配都会进入fid这个递归调用里。要想找一个比这还简洁的求斐波纳契序列的方法可真是不容易了。这是一种非常强大的技术。

原文:An Introduction to Functional Languages

作者:Ken Sipe