加入收藏 | 设为首页 | 会员中心 | 我要投稿 江门站长网 (https://www.0750zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 编程要点 > 语言 > 正文

编程的宗派

发布时间:2016-10-20 06:18:06 所属栏目:语言 来源:简书网
导读:副标题#e# 总是有人喜欢争论这类问题,到底是函数式编程(FP)好,还是面向对象编程(OOP)好。既然出了两个帮派,就有人积极地做它们的帮众,互相唾骂和鄙视。然后呢又出了一个好好先生帮,这个帮的人喜欢说,管它什么范式呢,能解决问题的工具就是好工具

富有讽刺意味的是,半壶水的Lisp程序员都喜欢用list,真正深邃的Lisp大师级人物,却知道什么时候应该使用记录(结构)或者数组。在Indiana大学,我曾经上过一门Scheme(一种现代Lisp方言)编译器的课程,授课的老师是R. Kent Dybvig,他是世界上最先进的Scheme编译器Chez Scheme的作者。我们的课程编译器的数据结构(包括AST)都是用list表示的。期末的时候,Kent对我们说:“你们的编译器已经可以生成跟我的Chez Scheme媲美的代码,然而Chez Scheme不止生成高效的目标代码,它的编译速度是你们的700倍以上。它可以在5秒钟之内编译它自己!” 然后他透露了一点Chez Scheme速度之快的原因。其中一个原因,就是因为Chez Scheme的内部数据结构根本不是list。在编译一开头的时候,Chez Scheme就已经把输入的代码转换成了数组一样的,固定长度的结构。后来在工业界的经验教训也告诉了我,数组比起链表,确实在某些时候有大幅度的性能提升。在什么时候该用链表,什么时候该用数组,是一门艺术。

副作用的根本价值

对数据结构的忽视,跟纯函数式语言盲目排斥副作用的“教义”有很大关系。过度的使用副作用当然是有害的,然而副作用这种东西,其实是根本的,有用的。对于这一点,我喜欢跟人这样讲:在计算机和电子线路最开头发明的时候,所有的线路都是“纯”的,因为逻辑门和导线没有任何记忆数据的能力。后来有人发明了触发器(flip-flop),才有了所谓“副作用”。是副作用让我们可以存储中间数据,从而不需要把所有数据都通过不同的导线传输到需要的地方。没有副作用的语言,就像一个没有无线电,没有光的世界,所有的数据都必须通过实在的导线传递,这许多纷繁的电缆,必须被正确的连接和组织,才能达到需要的效果。我们为什么喜欢WiFi,4G网,Bluetooth,这也就是为什么一个语言不应该是“纯”的。

副作用也是某些重要的数据结构的重要组成元素。其中一个例子是哈希表。纯函数语言的拥护者喜欢盲目的排斥哈希表的价值,说自己可以用纯的树结构来达到一样的效果。然而事实却是,这些纯的数据结构是不可能达到有副作用的数据结构的性能的。所谓纯函数数据结构,因为在每一次“修改”时都需要保留旧的结构,所以往往需要大量的拷贝数据,然后依赖垃圾回收(GC)去消灭这些旧的数据。要知道,内存的分配和释放都是需要时间和能量的。盲目的依赖GC,导致了纯函数数据结构内存分配和释放过于频繁,无法达到有副作用数据结构的性能。要知道,副作用是电子线路和物理支持的高级功能。盲目的相信和使用纯函数写法,其实是在浪费已有的物理支持的操作。

fold以及其他

大量使用fold和currying的代码,写起来貌似很酷,读起来却不必要的痛苦。很多人根本不明白fold的本质,却老喜欢用它,因为他们觉得那是函数式编程的“精华”,可以显示自己的聪明。然而他们没有看到的是,其实fold包含的,只不过是在列表(list)上做递归的“通用模板”,这个模板需要你填进去三个参数,就可以生成一个新的递归函数调用。所以每一个fold的调用,本质上都包含了一个在列表上的递归函数定义。fold的问题在于,它定义了一个递归函数,却没有给它一个一目了然的名字。使用fold的结果是,每次看到一个fold调用,你都需要重新读懂它的定义,琢磨它到底是干什么的。而且fold调用只显示了递归模板需要的部分,而把递归的主体隐藏在了fold本身的“框架”里。比起直接写出整个递归定义,这种遮遮掩掩的做法,其实是更难理解的。比如,当你看到这句Haskell代码:

foldr (+) 0 [1,2,3] 

你知道它是做什么的吗?也许你一秒钟之后就凭经验琢磨出,它是在对[1,2,3]里的数字进行求和,本质上相当于sum [1,2,3]。虽然只花了一秒钟,可你仍然需要琢磨。如果fold里面带有更复杂的函数,而不是+,那么你可能一分钟都琢磨不透。写起来倒没有费很大力气,可为什么我每次读这段代码,都需要看到+和0这两个跟自己的意图毫无关系的东西?万一有人不小心写错了,那里其实不是+和0怎么办?为什么我需要搞清楚+, 0, [1,2,3]的相对位置以及它们的含义?这样的写法其实还不如老老实实写一个递归函数,给它一个有意义名字(比如sum),这样以后看到这个名字被调用,比如sum [1,2,3],你想都不用想就知道它要干什么。定义sum这样的名字虽然稍微增加了写代码时的工作,却给读代码的时候带来了方便。为了写的时候简洁或者很酷而用fold,其实增加了读代码时的脑力开销。要知道代码被读的次数,要比被写的次数多很多,所以使用fold往往是得不偿失的。然而,被函数式编程洗脑的人,却看不到这一点。他们太在乎显示给别人看,我也会用fold!

与fold类似的白象,还有currying,Hindley-Milner类型推导等特性。看似很酷,但等你仔细推敲才发现,它们带来的麻烦,比它们解决的问题其实还要多。有些特性声称解决的问题,其实根本就不存在。现在我把一些函数式语言的特性,以及它们包含的陷阱简要列举一下:

fold。fold等“递归模板”,相当于把递归函数定义插入到调用的敌方,而不给它们名字。这样导致每次读代码都需要理解几乎整个递归函数的定义。

currying。貌似很酷,可是被部分调用的参数只能从左到右,依次进行。如何安排参数的顺序成了问题。大部分时候还不如直接制造一个新的lambda,在内部调用旧的函数,这样可以任意的安排参数顺序。

Hindley-Milner类型推导。为了避免写参数和返回值的类型,结果给程序员写代码增加了很多的限制。为了让类型推导引擎开心,导致了很多完全合法合理优雅的代码无法写出来。其实还不如直接要程序员写出参数和返回值的类型,这工作量真的不多,而且可以准确的帮助阅读者理解参数的范围。HM类型推导的根本问题其实在于它使用unification算法。Unification其实只能表示数学里的“等价关系”(equivalence relation),而程序语言最重要的关系,subtyping,并不是一个等价关系,因为它不具有对称性(symmetry)。

(编辑:江门站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读