你的分享就是我们的动力 ---﹥

日常化函数式编程

时间:2014-12-25 18:24来源:www.chengxuyuans.com 点击:

函数式编程(Functional programming,FP)并不像听起来那么难,也并非什么新事物。它不过是以不同方式使用一些您已熟知的工具,把您的意识转向之前可能从未过多考虑的代码的某些方面。人们为FP兴奋不已,因为它给作为程序员的我们每天要面对的大部分任务带来了更简单的代码。

那么,什么是FP呢?它是一种对函数的刻意关注,这些函数被用作抽象(组织任务)和构造不同程序的工具。您会说这并非什么新闻——确实,FP语言Lisp已经存在了50多年——但就像生活中的许多事情一样,您使用工具的方式说明了找出解决某个给定问题的方法的难易程度。

别紧张

使用FP需要学许多东西,要看清成排成批的行话后面的本质核心并不容易。本文不会触及单子(monad)和函子(functor)一类有着异域风情的工具,相反,我将介绍使用抽象和结构化函数的核心理念,避免把该问题与在JavaScript上下文中不具什么实际意义的那些工具和技术混为一谈。

抽象

让我们先来谈谈抽象。抽象允许我们在代码中一次处理一个问题,如同是面向对象编程(Object-Oriented Programming,OOP)的组成部分一样,它也是FP的组成部分。

我们来考虑一下把某个实时聊天应用中的用户分为在线和离线用户的做法,若不使用高阶函数来编写这一实现,那么我们最终会得到类似如下的代码:

在这里,您可看到,这段代码解决了两个问题:一是通过测试每个列表中的每个子项来拆分列表这一任务,一是实现一个测试来查看某个用户是否在线。前一个问题是所有分区共有的——就是分割操作本身,后一个问题——检测——则特定于每一分区。

我们来看看,在应用了高阶函数之后会发生什么事情。共有部分——把一个列表划分为“是”和“否”两部分——可被写成一个接受特定部分——检测某个子项的代码的函数。下面就是该函数看起来的样子:

通过把一个函数传给另一个函数,我们可以避免在每次需要分割时复制过程中完全通用的部分——分割操作,只需写下我们现在所关心的部分:分割函数。

编写partition函数是很非常简单的事情,我们借用最初的代码,找出其中因每一特定分割问题而发生变化的部分,然后使用我们传给partition的函数来替换掉它们:

这就是对某个共有问题和某个特定问题的任意混合体进行转换的算法(秘诀),找出每个特定问题的变化之处,然后把这部分移至某个包装了共有代码的函数的参数中。现在,我们已抽象出了共有问题,代码的余下部分变得更清晰易懂了——在更高的层面上关注问题的解决,就更接近当前要完成的特定任务。

若回想一下在OOP中我们是如何通过重写子类中的方法来特殊化通用超类的,您应能立刻明白这其中的相似之处。在FP版本中,我们传入的是已经重写的函数。

更多高阶函数

高阶函数最适合用在列表和键-值对象的日常操作中(在JS中我们把对象{ }用于此,再不久将用上Map)。若大致了解一下Array.prototype,您会找到一拨可用在编程中的有趣的高阶函数。我们来研究一下其中的一个。

在编程中用到某类事物的一个列表,以及希望最终得到另一类事物的列表,这都是非常常见的情况。比如说,我们拥有一个用户列表,我们希望从中提取出他们的离线时长这一数据。

在不使用高阶函数的情况下,我们编写的代码如下:

在使用高阶函数的情况下,我们用到了一个名为map的函数,有了该函数,我们的解决方案变成:

您可见到,我们不得不涉及的复杂性有望大大降低!此外,这种做法更能揭示意图——在前一段代码中,offlineForMilliseconds是否长于或短于users,这不是立刻能看穿的事情:我们知道这里用到了循环,但我们得看完全部内容才能弄清楚循环执行了什么类型的操作。相反,只要看到map函数,我们就知道,我们正在创建一个列表,列表为原始列表中的每个子项提供了一个新的值。

若使用新的ES6语法(或Coffeescript),那么在读或写代码时我们要做的工作何其之少就更是显而易见的事情:

现在,在需要通过一个现有的列表创造新列表时,我们立刻就会想到map。因此,我们已实现了程序的抽象——map代码抽象出了通用的映射机制,同时我们提供作为高阶函数的过程的非通用部分。

延伸阅读

高阶函数不仅适用于集合,他们也是抽象一些常见任务的普遍做法。可研究一下underscore.js,了解一下它们如何被用于调节函数调用时间这类事情(这对于保持UI简洁起到很大作用),也可看看async.js,在这一库中,高阶函数被用来流程化复杂的异步工作流。

避免不必要的变量

在OOP或过程式代码中,很多时候我们会共享在程序过程中会被修改的变量、属性和数据结构,为了表示部分已解决的任务,我们把一些子项添加到数组中、修改局部变量的值,或是修改对象的属性。

如上述的partition和map函数所示,在FP代码中,诸如此类的修改被隐藏在了函数的局部作用域内部。程序的不同部分在进行交互时,默认情况下它们不会分享可发生变化的变量或数据结构。

为什么要这样做?以JavaScript中的日期为例,考虑一下现实生活中的日期,您会发现它们非常类似数值。若把一天“加”到某个日期上,我们不用“修改”日期,只需从此开始指向一个新的日期即可:就像1+1不是把其中的一个1改成2。所以,在JavaScript中,我们把日期表示成一个可修改的对象是很奇怪的事情,因为它可修改,所以我们可能会制造出一些很诡异的错误。

比方说,我们有一个日期,我们把它传递给一个提醒函数,以安排一次提醒:

事件被安排在了什么时候?它被安排的时间比我们所期望的提前了4小时,因为Date对象在setupReminder函数中已被修改!JavaScript的Date对象是可修改的,实无此必要——把Date对象的行为变成与诸如数值一类的其他值一样,这要好得多,在这种情况下,加法返回一个新的日期而非修改后的当前日期对象。

对对象最初的打算是使用一个可修改的标识来建模某些事物——但它最终却被用来建模拥有无意义标识的值(您永远都不会问“哪一个2013年的圣诞节?”),这可能会导致一些错误和不必要的复杂性。

共享可修改的数据可能会导致一些问题,因为我们可能永远也无法确定程序其他地方的代码之后是否会对它们进行修改。想想为了避免这一问题,您应要克隆或复制某个对象的次数。避免可修改的数据或共享数据允许我们在程序中避免某一整类的潜在错误。

若可修改数据是某个给定问题的基本组成部分,那么FP显然不能完全消除它们。它仅是建议我们把管理状态的那部分程序分隔开来,尽量只把这些数据放在状态的必要部分中。所以,我们的Date会变成不变的“值”,而非像是OOP中通常使用对象进行建模的大量概念那样,是可修改的对象。同样,除非存在性能限制,否则我们使用不可修改的数据结构。

给对象留有位置

“对象”与编程中的许多用语一样,有多重含义。虽然FP JavaScript看似全然避免对象的使用,但我们仍使用它们,理由有三。

首先,我们把对象用作键-值数据结构:亦称字典、哈希或关联数组等。这与消息传递无关:关联键和值是一项基本任务,所有的FP语言都会支持键-值数据结构。

其次,我们使用对象来创建模块。我们把所有函数分开存放成键-值结构属性,以此来组织相关功能,而不是把他们全部放在一个扁平的全局名称空间中。同样,这也与消息传递的概念无关,我们仅是使用某种没有提供原生的模块概念的语言创建一个模块系统。

再者,我们把对象用于程序中那些需要可更换API的部分,这些API以状态化的处理或资源为中心。Promise(承诺)模式就是一个非常好的例子,Promise是一种用于表示处理结果的标准,理想情况下,您可以编写接受和使用承诺的代码,而又无需知道它使用的是诸 多 实现  中的哪一个。

在其他一些从头开始设计以便拥有更完整的函数式风格的语言中,承诺一类的概念可无需使用对象来表示。基于实用性理由,在编写FP JavaScript时效仿它们是不明智的——这会导致速度很慢、混乱以及可能是丑陋的代码。

结构化函数式程序

如何在不使用对象的情况下结构化程序?这取决于您正在编写的是哪种类型的程序。

就GUI而言,对象到问题的映射极为出色——这曾是OOP灵感来源的主要领域之一。若研究一下“面向对象”这一用语的发明者 Alan Kay对OOP的定义,我们会发现,对象应类似生物细胞:仅是通过消息通信,对消息的解释则留给接收细胞来完成。

GUI

所以,在GUI编程中,我个人常会使用对象来包装每种小部件,不过这些对象内部的代码会是函数式的。每次解释来自API的数据时,这或是为视图格式化一些值,或是创建一些实用工具来调节输入,在这些时候我会编写函数式代码。这是一种常见的模式——Michael Feathers和Gary Bernhard都已各自发现,“拥有函数式内核的对象”是一种联合使用两种范式的极好做法。

隐式对象

“拥有函数式内核的对象”这一用语可能并非代表您所设想之含义,它完全不要求在“类、实例和构造函数”这一意义上使用“对象”。在Alan Kay的定义中,“对象”是一个角色——一个接收外部世界的消息的物件。实际上,在JS或其他一些语言内部,对象不必是不同类型的实体:它们可能是各自独立的进程!对于Node.js项目来说,这是一种很常见的模式:响应来自外部的事件(消息)的进程内部拥有的是相当函数式的代码。所以,两个经由消息队列通信的进程显然满足Alan Kay对对象的定义,也因此,在前端,这表现为仅通过某种事件发射器连接的两个功能系统。

下一步

若这一泛泛而谈已经激起了您的兴趣,下面是一些非常不错的可以继续深入了解的内容:

  • Eloquent Javascript一书中关于FP一章
  • Node学校的FP作坊——一个使用Node.js的FP核心理念交互式教程
  • 由O’Reilly出版Fogus所著的Functional JavaScript一书
  • JavaScript Allongé——一本非常出色的书,介绍一些在FP JS中使用函数的复杂做法
  • 通过使用Lisp创建游戏来学习FP
  • 参加FunJS London活动,与我们一起写JS,活动每月的第四周在卫报的办事处举办一次。

天天使用函数式编程

FP可做到简单有趣,它提供了非常强大的抽象功能,更少涉及对象系统或可修改共享数据的复杂性。它有所不同,但本质上不过是以一种新的方式来使用极为熟悉的工具。我希望它能够帮助您在2014年建造出一些非同凡响的东西来!

关于作者

TIM RUFFLES

总部位于伦敦的SidekickJS ——JS代码质量跟踪器项目的创始人,Javascript Garden的维护者。Tim组织伦敦的FunctionalJS活动,在迷上了Clojure之后编写了许多JS;他会在一些大会上发言,并且参加了General Assembly公司和 EventHandler这一极客组织的教学活动。他喜欢攀岩、徒步、弹钢琴(弹得很糟),以及在酒吧里辩论。

您可在Twitter上关注Tim,可访问他的网站http://truffles.me.uk/

(由于译言编辑器不支持很好的代码排版,所以本文以图片方式显示代码,若想直接拷贝代码,请访问原文。)

转载注明地址:http://www.chengxuyuans.com/software_engineering/86223.html