你眼中的编程语言(你的编程语言能做到这个吗)
前言
最近从面相对象隐隐有点向函数式编程方向转变,碰巧看到一个国外哥们的一篇文章。觉得写的很有趣,便翻译过来。虽然语言采用的是JavaScript的,但是绝不影响阅读~
原文作者Joel Spolsky:Trello的联合创始人,Stack Overflow的联合创始人及现任CEO。
原文地址:https://www.joelonsoftware.com/2006/08/01/can-your-programming-language-do-this/
正文某天,你在浏览你写的代码时发现了两块代码几乎长得一模一样。比如:
alert("I'd like some Spaghetti!") alert("I'd like some Chocolate Moose!")
这两行代码唯一的不同就是 ‘Spaghetti’ 和 ‘Chocolate Moose’。他们是用 JavaScript 写的,但是你不用懂 JS 也能知道这些代码在干嘛。这两行代码当然看起来不对劲,你可以创建一个函数来优化下:
function SwedishChef(food) { alert("I'd like some " food '!') } SwedishChef('Spaghetti') SwedishChef('Chocolate Moose')
这个例子太过于简单了,不过你可以扩展想象,当代码过于复杂时,这种写法能带来的好处。你可能已经知道这些好处了,比如易读,易维护。抽象就是好!
然后你又发现有两块代码几乎长得一模一样,区别就是一块代码反复调用一个叫 BoomBoom 的函数,而另一块代码反复调用一个叫 PutInPot 的函数:
alert('get the lobster') PutInPot('lobster') PutInPot('water') alert('get the chicken') BoomBoom('chicken') BoomBoom('coconut')
现在你需要把一个函数传给另一个函数来让上面的代码好看点。函数接受函数为参数是编程语言的一个很重要的能力,它能帮你把代码中重复的部分抽离到一个函数中去:
function Cook(i1, i2, f) { alert('get the ' i1) f(i1) f(i2) } Cook('lobster', 'water', PutInPot) Cook('chicken', 'coconut', BoomBoom)
看!我们把函数作为参数传给另一个函数。
你的编程语言能做到吗?
等等……假设你还没有定义 PutInPot 和 BoomBoom,我们要是能直接把这两个函数行内传入,而不是先在别处定义这两个函数,不是很棒吗?像这样:
Cook('lobster', 'water', function(x) { alert('pot ' x) }) Cook('chicken', 'coconut', function(x) { alert('boom ' x) })
这真是太方便了。我随意写个函数就塞给另一个函数,都不用给入参函数命名。
一旦你开始思考把匿名函数当做参数传递,你可能会意识到处处可见的某种代码,比如,对数组的每一个元素进行操作:
var a = [1, 2, 3] for (i = 0; i < a.length; i ) { a[i] = a[i] * 2 } for (i = 0; i < a.length; i ) { alert(a[i]) }
操作数组的每个元素是个很常用的操作,你可以写个函数来帮你干这事:
function map(fn, a) { for (i = 0; i < a.length; i ) { a[i] = fn(a[i]) } }
然后你可以这样重构上面的数组操作代码:
map(function(x) { return x * 2 }, a) map(alert, a)
另一个常用的数组操作是把数组的每个元素按某种方式连接起来:
function sum(a) { var s = 0 for (i = 0; i < a.length; i ) s = a[i] return s } function join(a) { var s = '' for (i = 0; i < a.length; i ) s = a[i] return s } alert(sum([1, 2, 3])) alert(join(['a', 'b', 'c']))
sum 和 join 长得太像了,你可能想把它们的本质部分(把一个数组的所有元素按某种方式连接成一个值)抽象到一个通用函数里面去:
function reduce(fn, a, init) { var s = init for (i = 0; i < a.length; i ) s = fn(s, a[i]) return s } function sum(a) { return reduce( function(a, b) { return a b }, a, 0 ) } function join(a) { return reduce( function(a, b) { return a b }, a, '' ) }
很多老的编程语言根本就没办法做到上面展示的这些程序抽象。另外一些语言允许你这样干,但是很难做到(例如,C 语言有函数指针,但是你必须把函数声明和定义在其它地方)。面向对象编程语言没有被完全说服,开发者应该用函数来做任何事情。
Java 要求你先创建一个叫函子(Functor,PS:个人理解有点类似于interface的CallBack)的带有单一方法的完整对象,然后才能把函数当做一等对象。(PS:原文发表于 2006 年,当时 Java 8 还没有发布,lambda 表达式在 Java 中还不存在)。另外,很多面向对象语言要求你为每一个类创建一个文件,很快你的代码就变得笨拙臃肿。如果你的编程语言要求你写个函子才能实现函数一等对象,你就没有得到现代编程环境带来的一些好处。
就写个能帮你遍历数组的每个元素的函数而已,能给你带来什么好处?
我们还是回到前面提到的 map 函数。当你需要对数组里面的每个元素做某种操作时,这些操作的顺序可能并不重要。那么,假如你有两个 CPU,那你就可以写段代码让每个 CPU 计算一半的数组,这样 map 运行速度就两倍快了。
再假如,你有分布在全球各个数据中心的几十万个服务器,然后你有一个超级大数组,这个大数组包含了整个互联网的内容。那现在你就可以在这几十万台计算机上运行 map 函数了,每台计算机解决数组的一小块部分。
现在,搜索整个互联网的内容就简单到执行一个 map 函数,并给 map 传一个查询字符串就行了。
我希望你注意到的真正有趣的事情是,一旦你意识到 map 和 reduce 函数是每个开发者都能用的,你就只需要找个超级天才帮你写段比较难写的代码,让 map 和 reduce运行在一个大型的并行计算机集群上。然后你实现的这种分布式计算会比之前用 for 循环写的一次完成所有任务的老代码快无数倍。
让我再重复一遍。把循环这个概念从你的代码中抽象出去之后,你可以用任何方式来实现循环,包括用上面提到的可利用多余硬件来灵活伸缩的分布式计算。
现在,你应该明白了我为什么之前会抱怨现在的 CS 专业学生只学 Java:
如果你不懂函数式编程,你是不可能发明出 MapReduce 的(谷歌的高可伸缩搜索算法 )。Map 和 Reduce 这两个术语源自 Lisp 和函数式编程。如果你在学习 CS 6.001 时就学到了纯函数程序由于没有副作用,所以可以很简单完成并行计算,你是很容易理解 MapReduce 的(译者注:作者在抱怨现在 CS 教育缺失了函数式编程)。谷歌发明出了 MapReduce,而微软没有,说明了为什么微软现在还在试图弄出可行的搜索算法来赶上谷歌;与此同时,谷歌已经开始研发 Skynet 这个世界最大的并行超级计算机去解决下一个问题了。我认为微软还没搞清楚他们落后了谷歌多少。
好啦,现在我希望我已经说服了你,为什么支持一等函数的编程语言能让你找到更多程序抽象的机会,这意味着你的代码会变得更轻量,更紧凑,更易复用,和更可伸缩。很多谷歌的应用都用到了 MapReduce 算法。当有人优化 MapReduce 或者修复它的某些 bug 的时候,所有这些应用都受益。
现在我要变得感性一点了。我认为最有生产力的编程环境必须是那些允许你创建不同层级的抽象的语言。老而难用的 FORTRAN 根本不让你写函数。C 有函数指针,但你必须把函数声明和定义在其它地方,这样写太丑陋了。Java 逼着你用函子,更丑陋。
纠错:上次我使用 FORTRAN 还是 27 年前了。很明显它是支持函数的。我写到这里的时候肯定想到的是 GW-BASIC。
尾声编程语言博大精深,共勉,学习。
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com