Files
translations/pragmatic-functional-programming
2018-03-21 12:22:44 +08:00
..
2018-03-08 21:29:14 +08:00
2018-03-21 12:22:44 +08:00

原文链接: Pragmatic Functional Programming - Robert C. Martin (Uncle Bob)2017-07
基于liuchengxu的译文稿:实用的函数式编程

务实的函数式编程

函数式编程(functional programming/FP的风潮认真地来说是从大概10年前开始。我们开始关注像ScalaClojureF#这样的语言。这个风潮并非只是平平常常的『哇酷~一个新语言!』这样的热情。确实有某些实在的原因在推动着 —— 或者我们是这么想的。

摩尔定律告诉过我们每隔18个月计算机的速度就会翻倍。这个定律从60年代和2000年都是有效的。但是之后失效了。大家冷静想一下。时钟频率到达3G HZ以后不再变快进入平台期了。这已经触到了光速的物理极限在芯片上的信号传播已经快到不能有再快了。

所以硬件设计者改变了策略。为了获得更大的吞吐量,他们添加了更多的处理器(核心数)。同时为了这些核腾出空间,他们从芯片上去掉了很多缓存(cacheing)和流水线(pipelining)硬件。因而,单个处理器是比之前的要慢一些的;但是由于有了更多的处理器,吞吐量仍然是提升的。

【译注】:关于流水线(pipeline)参见指令流水线(instruction pipeline - zh.wikipedia.org

8年前我有了第一台双核机器两年后有了一台4核的机器。也就是说核心数开始进入了快速增加时期。那时候我们都认识到这将会以我们无法想象的方式影响软件开发。

一个对策就是学习FPFP会极力避免在变量初始化之后改变对其状态(state)。这对并发(concurrency)有着深远的影响。如果不能改变变量的状态,就不会有竞争条件(race condition)。如果不能更新变量的值(value),也不会有并发更新(concurrent update)的问题。

当然了,这曾被认为是多核问题的解决方案。当核心数激增,并发,甚至是共时性simultaneity),都会成为一个大问题。FP可以说是提供一个编程风格(the programming style可以减轻在单个处理器应对1024核时会出现的问题。

所以所有人都开始学习ClojureScalaF#或是Haskell;因为大家相信FP的冲锋号已经吹响,都想作好准备!

然而这一天一直没有到来。六年前我有了一个4核的笔记本然后我又有了两个4核。而我的下一台笔记本估计也是4核。我们又进入了另一个平台期了

说个题外话昨晚我看了一部2007年的电影。女主角在用笔记本在一个时髦的浏览器里面浏览网页在使用Google,以及用翻盖手机收发短信。一切都是那么的熟悉。只不过都过时了 —— 我可以看出笔记本是老型号浏览器是个老版本而翻盖手机比起今天的智能手机就像个古董。而这些方面是在2000到2011年之后有了翻天覆地的变化从1990到2000年才有变化。我们真的能预见在计算机和软件技术上现在是一个平台期吗

**_ TODO _**上面的这句比较级要好好推敲!

所以,或许,FP并不像我们之前想的那样是关键的一个技能。或许我们不会被那么多的核包围。或许也不用去担心在芯片上有32768个核。或许我们都可以放松一下回到以前那样去更新变量。

但是,我觉得这会是一个错误,一个严重的错误。我觉得这会和以前滥用goto一样严重。我觉得这会和放弃动态派发(dynamic dispatch)一样危险。

【译注】:关于动态派发(dynamic dispatch)参见动态调度(dynamic dispatch - zh.wikipedia.org

为什么呢?让我们回到起初引起我们兴趣的原因 —— FP使得并发变得安全得多。如果你要搭建一个有很多线程或是进程的系统,使用FP会大大减少可能由竞争条件和并发更新引发的问题。

还有其它理由吗?呃~FP更易写、易读、易于测试和易于理解。听到这些,我能想象到,有些人已经在挥舞着拳头和抛桌子了。当你尝试过FP,你发现一点也不容易。mapreduce和递归 —— 尤其是尾递归,有哪一个是容易的?是的,收到收到。其实使用这些是一个熟悉程度的问题。一旦你熟悉这些概念以后(这个熟悉过程其实并不需要太长时间),编程就会变得容易的多

为什么会变得容易得多呢?因为你不再需要跟踪系统的状态。由于变量的状态无法改变,所以系统的状态也就维持不变。不需要跟踪的不仅仅是系统,还有列表、集合、栈、队列等等通通都不需要跟踪状态,因为这些数据结构也无法改变。在FP语言中,当你向一个栈push一个元素,你将会得到一个新的栈,并不会改变原来的栈。这意味着减轻了程序员的负担,他们所需要记忆的东西更少了。需要跟踪的东西更少了,因而代码变的编写、阅读读、理解和测试。

那么,你应该使用哪种FP语言呢?我最喜欢的是Clojure。因为Clojure极其简单。它是Lisp的一个方言,Lisp是一个简单和漂亮的语言。让我给你展示一下:

Java中的函数:f(x)

转换成Lisp的函数就是简单地将第一个括号移到左边即可:(f x)

现在你已经学会95%的Lisp和90%的Clojure了。对这些语言而言,这些小括号就是全部的语法了。极其简单。

你可能以前见过Lisp程序,不过不喜欢这些括号。可能你也不喜欢CARCDRCADR这些。别担心。Clojure有着比Lisp更多的符号,所以括号相对少一些。Clojurefirstrestsecond代替了CARCDRCADR。此外,Clojure基于JVM,它完全可以访问Java库,和任何其他的Java框架和库。它的互用性快速而便捷。更好的一点是,Clojure能够拥有JVM完全的面向对象特征。

『等一下!』你可能会说,『FP和面对对象是相互不兼容的!』谁告诉你的?事实并非如此!在FP中,你的确无法改变一个对象的状态。但是那又怎么样呢?当你想要对一个对象进行改变时,得到一个新的对象就好了,之前的对象无须改变。一旦你习惯于此,这是十分容易处理的。

再回到面向对象。我发现面向对象最有用的一个特性是,在软件架构层面的动态多态性。Clojure提供了对Java动态多态性的完全接入。最好是用例子解释一下:

(defprotocol Gateway
  (get-internal-episodes [this])
  (get-public-episodes [this]))

上面的代码定义了一个JVM的多态接口(interface)。在Java中,这个接口看起来可能像这样:

public interface Gateway {
    List<Episode> getInternalEpisodes();
    List<Episode> getPublicEpisodes();
}

JVM这个层面,所生成的字节码是完全相同的。实际上,一个Clojure的写程序要去实现这个接口会像Java实现一样。一个Clojure程序会通过同样的token实现一个Javainterface。在Clojure中,看起来大概像这样:

(deftype Gateway-imp [db]
  Gateway
  (get-internal-episodes [this]
    (internal-episodes db))

  (get-public-episodes [this]
    (public-episodes db)))

注意构造函数参数db和所有的方法是如何访问它的。在上例中,接口的实现只是通过传递db简单地委托给了一些本地函数。

Lisp一样,Clojure也是一个同像性Homoiconic)的语言,也就是说,代码本身就是程序能够操作的数据。这不难看出。下面的代码:(1 2 3) 表示一个三个整数的列表(list)。如果该列表的第一个元素变成了一个函数,也就是(f 2 3),那么它就变成了一个函数调用。故而,在Clojure中,所有的函数调用都是列表。列表可以直接被代码操作。所以,一个程序也可以构造和执行其他程序。

最后说一句,FP十分重要。你应该去学习它。如果你还在想你应该从哪个语言学起,我推荐Clojure