阅读 1032

【译】你的编程语言能做到这个吗?(为什么要学函数式编程)

译者序

今天偶然发现公司里有同事发布了一个函数式深度学习框架,刚好最近我也在入门深度学习,所以去了解了一下函数式编程在深度学习里面的应用。在查资料时我找到了今天要翻译的这篇文章。

这篇文章作者是 Joel Spolsky。他是 Trello 的联合创始人,Stack Overflow 的联合创始人和现任 CEO。

正文

某天,你在浏览你写的代码时发现了两块代码几乎长得一模一样。比如:

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)
复制代码

看!我们把函数作为参数传给另一个函数。

你的编程语言能做到吗?

等等……假设你还没有定义 PutInPotBoomBoom,我们要是能直接把这两个函数行内传入,而不是先在别处定义这两个函数,不是很棒吗?像这样:

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']))
复制代码

sumjoin 长得太像了,你可能想把它们的本质部分(把一个数组的所有元素按某种方式连接成一个值)抽象到一个通用函数里面去:

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 要求你先创建一个叫函子的带有单一方法的完整对象,然后才能把函数当做一等对象。(译者注:原文发表于 2006 年,当时 Java 8 还没有发布,lambda 表达式在 Java 中还不存在)。另外,很多面向对象语言要求你为每一个类创建一个文件,很快你的代码就变得笨拙臃肿。如果你的编程语言要求你写个函子才能实现函数一等对象,你就没有得到现代编程环境带来的一些好处。

就写个能帮你遍历数组的每个元素的函数而已,能给你带来什么好处?

我们还是回到前面提到的 map 函数。当你需要对数组里面的每个元素做某种操作时,这些操作的顺序可能并不重要。那么,假如你有两个 CPU,那你就可以写段代码让每个 CPU 计算一半的数组,这样 map 运行速度就两倍快了。

再假如,你有分布在全球各个数据中心的几十万个服务器,然后你有一个超级大数组,这个大数组包含了整个互联网的内容。那现在你就可以在这几十万台计算机上运行 map 函数了,每台计算机解决数组的一小块部分。

现在,搜索整个互联网的内容就简单到执行一个 map 函数,并给 map 传一个查询字符串就行了。

我希望你注意到的真正有趣的事情是,一旦你意识到 mapreduce 函数是每个开发者都能用的,你就只需要找个超级天才帮你写段比较难写的代码,让 mapreduce 运行在一个大型的并行计算机集群上。然后你实现的这种分布式计算会比之前用 for 循环写的一次完成所有任务的老代码快无数倍。

让我再重复一遍。把循环这个概念从你的代码中抽象出去之后,你可以用任何方式来实现循环,包括用上面提到的可利用多余硬件来灵活伸缩的分布式计算。

现在,你应该明白了我为什么之前会抱怨现在的 CS 专业学生只学 Java:

如果你不懂函数式编程,你是不可能发明出 MapReduce 的(谷歌的高可伸缩搜索算法 )。Map 和 Reduce 这两个术语源自 Lisp 和函数式编程。如果你在学习 CS 6.001 时就学到了纯函数程序由于没有副作用,所以可以很简单完成并行计算,你是很容易理解 MapReduce 的(译者注:作者在抱怨现在 CS 教育缺失了函数式编程)。谷歌发明出了 MapReduce,而微软没有,说明了为什么微软现在还在试图弄出可行的搜索算法来赶上谷歌;与此同时,谷歌已经开始研发 Skynet 这个世界最大的并行超级计算机去解决下一个问题了。我认为微软还没搞清楚他们落后了谷歌多少。

(译者注:谷歌把 MapReduce 算法的论文开放了,你能在这里读到)

好啦,现在我希望我已经说服了你,为什么支持一等函数的编程语言能让你找到更多程序抽象的机会,这意味着你的代码会变得更轻量,更紧凑,更易复用,和更可伸缩。很多谷歌的应用都用到了 MapReduce 算法。当有人优化 MapReduce 或者修复它的某些 bug 的时候,所有这些应用都受益。

现在我要变得感性一点了。我认为最有生产力的编程环境必须是那些允许你创建不同层级的抽象的语言。老而难用的 FORTRAN 根本不让你写函数。C 有函数指针,但你必须把函数声明和定义在其它地方,这样写太丑陋了。Java 逼着你用函子,更丑陋。(见前译注)

原文纠错:

上次我使用 FORTRAN 还是 27 年前了。很明显它是支持函数的。我写到这里的时候肯定想到的是 GW-BASIC。

译者后记

JavaScript 是我入门编程的第一门语言,也是我目前掌握最熟练的语言。我一开始以为一等公民函数就是一个很普通的特征,其它语言应该也有,但直到最近我才知道它来自 Lisp,在主流编程语言里面还比较小众,目前只有一部分编程语言才在最近加入 lambda 表达式(我知道的只有 Java 和 Python)。

我不明白为什么有那么多开发者认为 JavaScript 垃圾。Lexical scoping(我不知道这个术语对应的中文翻译是什么)和一等公民函数的语言特性已经足够让你写出强大而复杂的应用,而这些强大的特性并不是所有主流语言都支持的,JS 怎么就垃圾了?

同样我也不明白为什么有人认为 JavaScript 不适合函数式编程。这段时间比较火的 “计算机之子” 对 JS 有这样的评价:

用 JS 做函数式编程并不靠谱,Map/Reduce/Redux/Hooks 等并不是函数式编程,只是长得像而已。

Hooks 借鉴了 Algebraic Effect,有些 FP 的影子,但太杂糅了。Redux 是直接从 Elm 借鉴过来的,不知怎么就不是函数式。而 map 和 reduce 刚刚已经说得很清楚了,是函数式编程的核心概念,原理上也和 Lisp 一致,怎么就不函数式了?

JavaScript 是支持多个编程范式的。Vue 的成功已经证明了 JS 在面向对象程序设计上的潜力,但这并没有证明 JS 不适合写函数式代码。React 比较函数式,但为了照顾开发者的接受程度,做出太多妥协,它本可以更函数式。

用时下开发者的接受程度来判断一个编程语言是否具有某些特性显然是荒唐的。真这样的话,React 不会探索出这么多新的可能。如果你理解你在干什么,你只需要 JS 提供给你的一些核心能力就实现程序功能。你并不需要使用 Proxy(也不需要 defineProperties), generators, iterators 等新功能,你甚至都不需要原型链继承。而一等公民函数,是提供给你这些能力的核心特性。(我只是说理论上你不需要,没有鼓励你和整个开发生态为敌)


关注下面的标签,发现更多相似文章
评论