Herb:我想,我们有必要在“函数型”编程领域做一个进一步区分,将其划分成两个部分。我非常同意Anders和 Erik的意见。我不太同意的是这样的措辞:我们之所以继续使用“命令型”编程语言,是因为这是大家目前所能理解的;通用程序员目前的工作并未取得巨大的成功;市场对于“所有的东西都是表达式,所有的语言都应该是表达式类型的语言”这样的理念已经非常接受了;“函数型”语言是“串行执行”的好药方。我们要想使“函数型”语言运转良好,关键的并不是处理好基本的表达式问题,而是处理好lambda表达式和副作用的问题,关键是能够将表达式作为第一级的编程要素来使用——LINQ也是最近才在做,关键是能够指出lambda表达式和Closure(译者注:函数型编程语言中的一个概念,可以方便地组合函数,返回函数)的副作用。实际上,最后这点目前是缺失的(Anders也附和着:对,对)。这些东西在“命令型”语言中也是要处理的东西。我为什么提这些?因为我觉得说“函数型”语言是方向,目前的“命令型”语言不够好,因此是垃圾,必须要抛在脑后,全面采用“函数型”语言这样的说法不对(译者注:呵呵,对 Anders的说法有点急了,毕竟是泡在C++上,对C++有感情的人)。我认为,对于“函数型”语言能够帮助程序员完成哪些工作,目前还不太明了。比如,能够用它写通用代码吗?能够用它系统级代码吗?当然,“函数型”语言有不少我们能够应用的好东西,比如lambda表达式,比如Closure,C# 借鉴了,C++也在借鉴,这些语言因此增色不少。关于“函数型”语言还有另一个问题,那就是有两种类型的“函数型”语言,一种是没有副作用的,因此就没有共享的易变的状态的问题;一种是人人都在使用的,对吧(译者注:显然Herb认为“没有副作用”的理想情况是不太可能的)?因为你不太可能说,“瞧,我是完全并发安全的,因为每次我从XX(译者注:听不清)向量中得到一个拷贝,或者我使用XX(译者注:听不清)元素的时候,我都是取得一个拷贝”。确实不错,这里是没有共享的易变的状态,但是是否能够完全并发安全则不一定。
Anders:是的。我的意思是,在类似C#或VB这样的“命令型”编程语言中加入“函数型”结构,能给我们提供“以函数型风格”写库的能力,从而我们就能够非常明确地说,如果你能保证传入的lambda表达式是纯粹的函数,我们就能保证正确地把它分散到若干个线程或者CPU上,最后把它综合起来,给你一个正确的结果,我们能够保证代码运行得更快,同时你还不用作任何编码上的修改。如果你在写一个大大的For循环,我们永远都不可能保证做到前面所说的,此时,“函数型” 编程能够提供给你的是一系列表达式,再加上“把代码当作参数传递”,“类型推论和泛型编程可以正确地绑定所有的类型”这些特性,这样你就能更方便地编写 “可组合的算法块”。
Charles:这样一来不就削弱了抽象吗(译者注:Charles可能想的是程序员不需要再关心“可组合性”,语言和运行库应该保证这件事,而现在听起来并非如此)?
Herb:呃,我很同意Anders的意见,我想指出的是,当前所有的语言都有意不保证 “没有副作用”。之所以如此的原因是,除非所有的语言都添加一些机制让程序员可以清除副作用,我们这些做语言的人不敢打这个包票。但是,添加这样的机制涉及到众多参加者,大家一起思考、讨论什么是最好的方法的过程会很漫长。我们所做的是相信程序员,因为我们自己不知道。然而,程序员在很多情况下也不知道,因为他写的函数要调用其他的库。这里“可组合性”又浮上水面了,程序员根本不知道他用的库有怎样的副作用。一般说来程序员会再增加一层间接性,但是问题依然存在,没有人能够清楚地知道副作用,除非他拥有涉及到的所有的代码,这就是难题所在。上面这些讨论对“锁”也适用,因为“锁”也是个全局问题,对于“可操作性”是个障碍。
Brian:(译者注:在Herb说话的时候已经很着急地想说了几次)在这点上Haskell做得很好,Haskell是“永远没有副作用”的范例。
Erik:是的,但做到这点的过程也是痛苦的,因为并非所有的情况都一目了然。一旦你的(库)代码有副作用,而且因此使程序员的代码必须按照某种顺序执行(因为副作用的关系,该程序必须先干某事,再干某事),某种意义上你在用汇编语言编写东西,因为程序员将不再能用“表达式+表达式”的方式来写代码,他必须决定先对某个表达式求值,再对另一表达式求值,再把值加起来。因此我认为我们在这点上干得还是不够漂亮。
Brian:现在,我们在“流库”上有例子。好消息是,我们已经有Haskell向你展示如何以“可行性”方面的代价,换来用绝对纯粹的方式来做事。当然,除Haskell外我们有各种“杂牌”语言。呵呵!
(众人均乐)
Charles:这是个供研究的语言吗?
Brian:是的,我们将它设计为供研究用。
Anders:没有纯粹的好或坏,我认为,虽然进展缓慢,我们仍然快到一个令人满意的中间点了。我完全同意说,如果我们确实能够保证函数的纯粹性,生活将会非常美好。最终我们必须要做到。
Brian:在研究领域,大概有20多项工作与此有关——契约语言,契约和限制,等等。
Erik:但是,不少的副作用也并非坏事,如果我的函数使用了一个局部变量,这就是使用了一个状态,但是,函数本身还是纯粹的。如果你想要完全避免副作用,我觉得会非常困难,一些东西可以是局部不纯粹而整体纯粹的。
Herb:回过头,让我们从整体上看看“可组合性”。让我吃惊的一件事是,很多时候,人们甚至都没有意识到这是个问题。他们并没有意识到自己实际上经常碰到这个问题。整个软件工业,整个世界其实已经基于可组合的软件了。在硬件会议上,我经常对硬件公司提到的是(呵呵,通常此时我都是在轰击硬件工业,但是软件业也有同样的问题):硬件的并发问题被仔细地探索过了,而且,当前消除共享易变状态的最好办法就是“锁”;但是,锁是全局的,是一种全局资源,不能被组合;“被锁”是经常发生的事情,而拥有一个锁时,我还能调用任何其他的未知的代码,这就破坏了“可组合性”。说到这里,有的听者往往一脸茫然:这有什么问题吗?我于是指出,好的,你们是否上网下载别人刚刚发布的,自己喜欢的新软件,比如,某个浏览器,3个插件,然后就用呢?大家回答:是啊。于是我再指出,你们是否意识到了,当你们这样做时,经常地,这些软件都是第一次在最终用户的机器上被组合,被使用?既然如此,你们怎么可能对其进行测试?这时,屋子里有百分之十的人会露出恍然的表情,因为此前他们没有想过这个问题:这些软件是第一次在最终用户的机器上被组合,我们怎么进行测试?正因如此,“可组合性”是更加重要的一个问题。更不用说我们现在有AJAX,应用程序,以及众多的其他插件经常被下载,而且被要求在同一个用户界面中协调工作。
Charles:你这么一说,关于“函数型”编程,我马上想到一个问题是:在现有基础上再加一层必须考虑的抽象,实际上能不能增加程序员的生产率,是否真的有帮助?作为程序员,现在还要考虑“副作用”的问题。反正我现在用C#还是其他语言编程的时候,是不会像一个“函数型”程序员那样考虑副作用的。
Herb:往一个语言上增加更多的特性无法使其变简单,这是个我们面临的基本难题。
Anders:作为一个语言设计师,对于编程语言,我们所能做的是——减缓新特性不断累积的速度,从而避免最终的倒塌。我想说的是,你永远不能收回某个特性。理论上,你可以收回某个特性,实际中,你不能这样做,因为后果是若干代码的崩溃,这行不通。
Brian:是的,在从VB6到VB.NET的过程中,很多人已经感到饱受折磨了。是的,(微软的)解决方案是说那(VB.NET)是个完全不同的语言。但是,我认为,“函数型”编程的概念已经出现了不短的时间,新毕业的程序员至少听说过这个概念。他们在Python、JavaScript或者其他什么地方见过 lambda表达式,或者他们学过Scheme,总之这不是个没听说过的东西,因此,当他们在C#或其他的更传统的编程语言,如VB或C++中看到这些概念的时候,他们并不会感到畏惧,因为他们了解这些概念的优点,他们知道这些东西如果用得正确,可以增加代码的可组合性。你知道,传统语言的可组合性靠的是程序员的自觉。在某些方面,Haskell编程比“命令型”编程要痛苦得多,然而反过来说,它也比“命令型”编程要简单些,因为你不会把事情弄得一团糟。呵呵呵。
Anders:这里我想插一句。我认为,在很大程度上,我希望我们在C#或VB上做的工作至少是——在很大程度上对“函数型”编程进行“去神秘化”。学院式的人总是倾向于让事情听起来比实际的要复杂,因为这样一来就显得他们自己比较聪明。“呃,我并不是针对任何人,但是,这里有不少符号代数和其他东西呢!(译者注:音频提高了,似乎在学某些人的语气)”。当我向人们解释lambda表示式时,大家是另一种感觉:“你的意思是他们就是函数?这有什么新鲜的?我们早就有函数了,lambda表达式只是些语法糖衣外壳吧?”是的,lambda表达式确实只是语法糖衣外壳,但这是种强有力的语法糖衣外壳,它使你能按照一种新的方式去思考问题,以前总是存在的一些语法噪音——必须声明函数,进行委托,等等——就会逐渐减少,最终,一个全新层次的表达式列表会产生出来。这才是关键所在!围绕着“函数型”编程还有非常多神秘,我希望我们能够(在打破神秘上)有所突破……
Herb:关于(Brian)说的教育问题,就是关于人们能够在学校中学到的东西的说法,我不太同意。我真的希望情况确实如此,但是我认为缺乏证据,或者说除了 “Pascal熟悉者”之外,我们无法获得更多。不管是在学校还是在今后的职业生涯中,人们都认为自己接触了多种语言,因为他们用过C、C#、C++、 VB以及Pascal。但是,这些语言本质上是一样的,是同一族的语言。人们并没有接触过APL——编程就象在解释器外壳(译者注:可以想象DOS的命令行)上敲东西;不熟悉Prolog——任何东西都是规则;不知道Lisp——所有的都是链表。只有当你深入了2到3种这些东西后,你才知道,并不是所有的东西都是命令。也许在你今后的职业生涯中用的都是命令型语言,正如我们大多数人一样,但是,你不会对其他看待世界的观点一无所知。某人从小到大,如果没有离开过成长的城市,如纽约,超过一英里的话,会错过不少有趣的地方,不管你是否想在其他地方住下来,你应该知道它们,体验它们。
Brian:呃,我觉得这是个渐变的过程,当然,我确实观察到年轻一代程序员对于“函数型”编程有更好的认识了。你知道,10年,15年之前,如果你和人们谈起 Scheme时,人们会感到不解地看着你,“什么?你在说什么?”。现在,人们听说过了,他们见过“函数型”编程的关键字。而且,通过 JavaScript和Python,人们尝试了“函数型”编程,也使得这些东西更加流行。我并不是说已经出现了巨大的变化,我是说逐渐的变化和更多的认知,人们不再象以前那样看到这些东西就害怕了。
Erik:也许JavaScript是这个世界上最……(译者注:Brian一阵激动的插嘴,搞得Erik的话听不清了)。我不确定当人们使用JavaScript时,他们是否意识到了这是一种把“函数”当作第一要素的语言。
Brian:你可以在运行的时候创造它们(函数)……
Charles:所有的东西都是一种类型,所有的东西都是对象,是吧?这是个有趣的语言。随着整个互联网的发展,你认为这种语言也应该有所进化……?
Brian:这里的“所有的东西都是对象”,不是面向对象的意义。这里是从“任何东西都是关联链表”的角度来说。现在我听起来像一个老Lisp编程者(译者注: Lisp基于链表),是吧?一个JavaScript的对象不像一个C#对象那样——所有的内存排列都决定了,什么东西都静态地决定了。这意味着所有的东西都可以添加、拿走,可以违反规则,它确实只意味着一个关联链表。
Anders:属性包(我得意地笑)……
Brian:是的,属性包的说法更好!
Erik:是的,值可以是函数、其他对象,或者是其他属性包。
Brian: JavaScript其实就是源自Lisp的Scheme。我又在说Scheme,因为我也许是这个屋子中唯一老到听说过这个词的人,呵呵!但是, Scheme有点类似于“上帝的Lisp”(译者注:不知道是God’s Lisp, Guard’s Lisp还是什么别的,懂得有限,暂时按God’s Lisp处理,达人请指教),所有的噪音都被消除了,只剩下最基本的、最少的东西。JavaScript就是把这些东西带到了前台,披上件不那么可怕的外衣的结果。因为JavaScript看起来有点“C”的味道,你可以使用花括号!呵呵!
(一阵乱哄哄)
Charles:但是,JavaScript只是一种解释性语言,而且,对开发者来说,用它编程也不是很有效率。JavaScript不像C#或VB,它没有一种真正的面向对象语言所应该具备的IDE,以及你可以工作于其上的框架。
Anders:呃,JavaScript是一种弱类型语言,或者说动态编程语言。人们经常把“编译时”与语言的“类型强弱”相提并论。但是,这两个概念其实是相互独立的,是吧?你可以有一种“强类型”,然而在运行期编译的语言,如果你真想要这样的东西的话。但是,对我来说,我以前也讲过,我非常乐于向那些喜欢“动态语言”、“脚本语言”的人指出,正是他们所醉心的那些地方会有问题。经常地,人们都理所当然地认为,如果没有类型碍事,就不用声明什么东西了,可以随手就用,诸如此类。这样干的时候你确实能够更快地写程序,然而,这里有一个陷阱:因为没有类型,我们(编译器)给你提供的帮助就少得多。当你写“X.”时,对不起,“.”之后是什么我们没法告诉你,我们没有足够的智能显示给你,因为我们没有任何办法知道将发生什么。有时我们可以猜,但我们有可能猜错,有时我们根本就一无所知,对吧?X或者其他参数,我们不知道将发生什么。我发现,有趣的是,我们在C#中进行的类型推论的工作,在许多方面都使得C#编程看起来更像动态语言,因为不会随时看到类型,甚至压根看不到。但是,类型在那里,静态地在那里,这也意味着,我们仍然可以给你提供“语句完成”这样的智能帮助。
Charles:你是在说“var”关键字啰?
Anders:“var”关键字只是个例子。光看这个关键字,似乎这是动态类型,然而,这是静态类型。当你定义“var Blar”时,我们知道“Blar”是什么类型;当你写“Blar.”时,“.”之后我们可以为你显示出东西。
Herb:我们(C++)中也在做一样的事情。我有一个“int”类型的vector,我从该vector的头部取出一个元素,我有什么必要再用 vector<int> 来声明一个遍历器?编译器知道它是这个类型,编译器能够将类型正确加到变量上。这就是“var”类型干的事,你不必到处都写出类型信息。这带来了很大的好处,你可以书写更好的“范型”代码,以前那些东西是写“范型”代码的障碍。
Anders:我想说的是,(如果有类型),我们就能在你写代码的时候给你许多的帮助,尤其在“智能感知”上我说是大实话。当今世界,如果你想把“智能感知”去掉,人们绝对会大叫“不!不!”,呵呵,这工具太有用了。但是,这里还有性能的问题。我是说,当编译器知道类型时,它能够为你生成更好的代码。就是说,你会得到更好的执行效率。
Erik:这里有件趣事。几个月前我们有次编译器XX(译者注:没听清),参加者都反映说,“喔,你知道,F#是最好的动态语言!”要我来说的话,F#是拥有最先进的静态类型系统的语言。当然,你不用写任何的类型,因为编译器帮你推断了所有的类型。很有趣的是,人们经常混淆“不用写类型”与“动态类型”。
Brian:这些人可都是写编译器的职业程序员!这是在课程结束时,做调查时出的趣事:“你最喜欢的动态语言?”“F#!”
Charles:但是(结巴了一阵,可能是震惊了),从开发人员的角度来看,动态类型是设计期的事吧?就我自己而言,如果我不必对任何东西进行强类型处理,我就会假设是动态类型。这是不是错了?我是说,编译器是怎么弄的?
Anders: “动态类型”和“隐式类型”是有区别的。一种情况是,靠编译器推断出类型,但编译器在编译期就推断出来了。另一种情况是,编译器在编译时一无所知,它假设这东西可以是任何类型,然后在运行时的适当时机,检查到底是什么类型。后一种情况下,当你分发一个虚函数调用时,也许会使用一个查找表,或是别的什么东西,因为你压根还不知道那是什么东西。前一种情况,根据到底是C#或是C++,你可以预先构造好虚表,准确地知道使用这个虚表的哪个内存地址来找到实际的东西,因为我已经计算好了。这就是预先有效地计算好了所有的东西。但是,人们往往错误理解(静态类型)为,“我,我这个人本身,必须在任何时候亲自手动写出类型”实际上这并不必要,因为类型可以被推断出来,如果你在一个地方知道了类型,你可以跟踪程序逻辑,根据这里发生了什么,类型是如何变化的,从而知道在另一个地方类型是什么。如果你是个足够聪明的编译器,你应该可以做到这些,是吧?我们正开始把这种“聪明”加入到程序语言中。
Brian: F#可能是这方面最好的例子。F#甚至欺骗了职业编译器程序员!我认为人们真正喜欢的是快速的反馈。人们喜欢快速看到结果。在他们的意识中,他们把这种喜好和“动态”相混淆了。下面我具体讲讲:你输入了一个函数,然后马上敲入一行代码来测试刚写的函数,如果能够正常工作,你就可以放心地忘掉它们(译者注:指可以投入下面的其他工作),这是人使自己的“大脑堆栈”保持满负荷运作的方式,是人的力量。历史上,“动态语言”在使人以这种方式工作上卓有成效。你打开一个Lisp,你实际上得到的是一个Lisp的监听器,你键入代码,然后在相同的环境中立即测试代码。但是,这实际上是个工具问题,与编译器什么的完全没有关系。你能够使动态语言有一个好的IDE和交互的开发环境,你也能使静态语言,如F#,拥有这样的IDE和交互的开发环境。这就是Sam在F#演示上干的事情。他打开好长一段代码,用鼠标选择它们,用“GO”按钮来执行;然后他打开另外一段代码,再用鼠标选择,再按“GO”来执行。所有的人都以为这是动态解释执行的。实际上,这是静态的,完全编译的。这就是欺骗行家的方法。呵呵(我得意地笑……)。但这确实是人们喜欢的东西。大家都喜欢能提供“不断反馈”的好工具,或者是“交互式开发环境”,无论你管这叫什么。只要你能够随时写代码并且测试,你就不必写一大堆代码后再测试。你会这样想,“嘿,我写了个叫‘RandomDouble’的函数,让我把它敲进去看看是否能工作”。如果能正常工作,你就可以在进一步的Debugging前把它忘掉(去做别的工作)。