JavaScript两大支柱-PART2:函数式编程

JavaScript是有史以来最重要的编程语言之一,不仅仅是因为它的流行,而且因为它推广了两个对编程发展极为重要的特性:

原型继承(没有类的对象,原型委托,又名OLOO - 链接到其他对象的对象),和
函数式编程(由带闭包的lambdas启用)

总的来说,我喜欢将这些范例称为JavaScript的两大支柱,我并不羞于承认它们已经影响了我, 我不想用没有它们的语言编程。

我们今天正在构建未来

我滑到冰球将要在的地方,而不是它已经在的地方。~ Wayne Gretzky

高科技的世界建立在不断创新的文化之上。 我们因为共同的需要而推动技术不断超越前沿,从而一起建立了伟大的事物。每一项新的突破使我们能够做我们几年前从未想到过的事情。 欢迎来到技术的世界。

不仅事情变化很快,事情变化的速度也在迅速变化。 过去几十年,许多科技迭代进入指数级的科技爆炸。 五年前我们构建典型应用程序的方式与我们今天构建应用程序的方式截然不同。 您现在可能正在使用2年前不存在的工具。 回到15年或20年,甚至我们的流程也完全不同(瀑布与敏捷等等)。

你花了两年时间学习Angular吗? 如果是这样,我会感到痛苦。 您学到的很多东西都不适用于Angular 2.0或React。 如果我们继续以我们一直以来的方式做事,这种变化的速度是不可持续的。

如果从长远来看我们将作为工程师生存下去,我们必须学习的最重要的事情是如何适应,以及如何使我们的代码更具适应性。

我们必须快速学习,因为我们明年使用的技术可能比现在要复杂两倍。 随着物联网和人工智能继续爆炸,我们应用程序的负载将继续呈指数级增长。 明天的应用程序需要比现在更具可扩展性,可互操作性,并发性,连接性,高性能和智能性。

我们能够跟上指数级增加的复杂性的唯一方法是降低理解程序的复杂性。 为了维护明天的巨型应用程序,我们必须学会构建更具表现力的代码。 我们必须学会编写更容易推理,调试,创建,分析和理解的程序。

面向过程的编程和面向对象的编程不会带领我们到我们需要去的地方。

在接下来的几年中,我们编码的方式将以激进的方式发生变化,推动我们进入一个根本不同的方向,而不是过去30年我们一直在猛冲的方向。 这些变化将在编程技术,流程,应用程序可扩展性和质量控制方面带来许多重要突破。

精通函数式编程的开发人员在不久的将来会有很大的需求。

这种变化已经席卷了Netflix,Facebook和微软等组织,并且编程风格的这种构造转变是有非常充分的理由的。

在佛教哲学中,禅宗公案经常被用来挑战学生对现实的基本理解,其中许多突出介绍了两个看似矛盾的想法和意义。当学生意识到两种观点的真实性时,就可以说掌握了这个知识或者说得到了启发。这是我的书《编写JavaScript应用程序》中包含的现代版本:

尊敬的大师Qc Na和他的学生安东一起走路。 安东希望抛出一个话题和大师讨论,安东说:“师父,我听说对象是一件非常好的东西 - 这是真的吗?”Qc Na怜悯地看着他的学生并回答道,“愚蠢的学生 - 对象只是一个穷人的闭包“。
由于被严厉地批评,安东从他的主人那里离开并回到他的房间,打算研究闭包。 他仔细阅读了整个“Lambda:The Ultimate …”系列论文及其表兄弟,并实现了一个带有基于闭包的对象系统的小型Scheme解释器。 他学到了很多,并期待他的老师能看到他的进步。
在他与Qc Na的下一次行走时,安东试图给师父留下好印象,说“师父,我已经努力研究这件事,并且现在明白对象真的是一个穷人的闭包。”Qc Na用他的棍子敲了安东,回应道 “你什么时候去学习? 闭包是一个穷人的对象。“那一刻,安东开悟了。

与对象一样,闭包是一种包含状态的机制。 在JavaScript中,只要函数访问在立即函数作用域之外定义的变量,就会创建闭包。 创建闭包很容易:只需在另一个函数中定义一个函数,然后通过返回或将其传递给另一个函数来暴露内部函数。 即使在外部函数完成运行之后,内部函数使用的变量也可用。

在JavaScript中,你可以通过工厂函数来使用闭包创建数据隐私:

1
2
3
4
5
6
7
8
9
10
11
var counter = function counter() {
var count = 0;
return {
getCount: function getCount() {
return count;
},
increment: function increment() {
count += 1;
}
};
};

在JavaScript的两大支柱第1部分中,我描述了我们大多数人所知道的面向对象编程的一些主要缺点,但也指出了面向对象设计的有更光明未来的一线希望:基于原型的面向对象设计 - 不仅仅是原型委托和链接的概念,还有构建不是来自类的新实例的概念,而是来自较小对象原型的汇编。

在JavaScript的两大支柱第2部分中,我们将讨论一种完全不同的编程范式,随着现代编程语言添加越来越多的功能以减少样板代码,重复和语法噪声,这种范式将在未来发挥更大的作用: 函数式编程(FP)。

“……前进的道路有时会回来。”~ The Wise Man, “Labyrinth”

在计算机革命的最初阶段,在微处理器发明几十年之前,一个名叫Alonzo Church的人在理论计算机科学方面做了开创性的工作。 您可能听说过他的学生和合作者,Alan Turing。 他们一起创建了一个名为Church-Turing Thesis的可计算函数理论,它描述了可计算函数的本质。
从这项工作中产生了两种基本的计算模型,著名的图灵机和lambda演算。

今天,图灵机经常被引用作为现代编程语言要求的基线。 如果语言或处理器/ VM指令集可用于模拟通用图灵机,则称其为图灵完备。 图灵完备系统的第一个著名的例子是lambda演算,由Alonzo Church于1936年描述。

Lambda演算继续激发了第一个高级编程语言之一,以及当今常用的第二古老的高级语言:Lisp。

Lisp曾经(并且仍然)在学术界极具影响力和流行,但它在生产力应用程序中也非常流行,特别是那些处理可扩展矢量图形等连续数据的应用程序。 Lisp最初是在1958年指定的,它仍然经常嵌入许多复杂的应用程序中。 值得注意的是,几乎所有流行的CAD应用程序都支持AutoLISP,这是AutoCAD中使用的Lisp方言,其中包括许多专业衍生产品,它仍然是世界上使用最广泛的CAD应用程序。

连续数据:必须被测量而不被计数的数据,并且可以在一定范围内取任何值。不同于离散数据,可以对其进行计数和准确索引,且 所有值都来自一组特定的有效值。 例如,小提琴可以产生的频率必须由连续数据表示,因为弦可以沿指板的任何点停止。 钢琴可以产生的频率是离散的,因为每根琴弦都被调到特定的音高,并且不能根据表演者的意愿在不同的点停止,钢琴键盘上通常有88个音符。 不可能计算小提琴演奏者可以产生的音符数量,小提琴演奏者经常通过在不同音符之间向上或向下滑动音高来利用它。

Lisp及其衍生产品诞生了一整套函数式编程语言,包括Curry,Haskell,Erlang,Clojure,ML,OCaml等…
像许多这些受欢迎的语言一样,JavaScript已经将新一代程序员暴露给了函数式编程的概念。
函数式编程非常适合JavaScript,因为JavaScript提供了一些重要的特性:一级函数,闭包和简单的lambda语法。
JavaScript可以很容易地将函数分配给变量,将它们传递给其他函数,从其他函数返回函数,组合函数等等。 JavaScript还为原始类型提供了不可变值,并且这些特性使得返回新对象和数组变得容易,而不是操纵作为参数传入的属性。
为什么所有这些那么的重要?
函数式编程术语表包含大量的大词,但从本质上讲,FP的本质非常简单; 程序主要由一些非常小的,非常可重用的,非常可预测的纯函数构建。
纯函数具有一些属性,使它们非常可重用,并且对于各种应用程序非常有用:
幂等性:给定相同的输入,无论调用函数的次数如何,纯函数总是返回相同的输出。 您可能听说过用于描述HTTP GET请求的这个词。 Idempotence是构建RESTful Web服务的一个重要特性,但它也有助于将计算与依赖于时间和操作顺序的计算分开 - 这对于并行和分布式计算(想想水平扩展)非常有价值。
由于幂等性,当您需要对连续数据集进行操作时,纯函数也非常适合。 例如,视频或音频中的自动淡入和淡出。 通常,它们被设计为与帧速率或采样率无关的线性或对数曲线,并且仅在最后可能的时刻应用以产生离散数据值。 在图形中,这类似于可缩放矢量图像,如SVG,字体和Adobe Illustrator文件。 关键帧或控制点相对于彼此或时间线布置,而不是键入固定像素,帧或样本。
因为幂等函数不依赖于时间或样本分辨率,所以可以将连续数据视为无界(几乎无限)的数据流,允许跨时间自由缩放数据,(采样率),像素分辨率,音量, 等等。
免于副作用:纯函数可以安全地应用而没有副作用,这意味着它们不会改变任何共享状态或可变参数,除了它们的返回值之外,它们不会产生任何可观察的输出,包括抛出的异常 ,触发事件,I / O设备,网络,控制台,显示器,日志等……
由于缺乏共享状态和副作用,纯函数不太可能相互冲突或导致程序的不相关部分中的错误。
换句话说,对于使用面向对象编码的程序,纯函数比没有函数纯粹性的对象产生更强的封装保证,并且这些保证提供了许多与面向对象封装相同的好处:在不影响程序其余部分的情况下更改实现的能力, 自我记录的公共接口(函数签名),独立于外部代码,能够在文件,模块,项目等之间自由移动代码等等。
函数纯粹性是函数式编程对面向对象编程中大猩猩/香蕉问题的回答:

“面向对象语言的问题在于它们带有所有这些隐含的环境。 你想要一个香蕉,但你得到的是拿着香蕉的大猩猩和整个丛林。“~ Joe Armstrong

显然,大多数程序都需要生成输出,因此复杂的程序通常不能仅使用纯函数来编写,但是只要它是实际有用的,那么将函数设置为纯函数是一个好主意。

用函数式编程工作

函数式编程提供了几种可以重用的功能。各种实现将使用不同的集合,它们有着不同的名字。 下面这个列表主要来自Haskell文档,Haskell是一种比较流行的函数式语言,但在几个流行的JavaScript库中你会发现类似的函数:

列表常用工具:

  • head() - 得到第一个元素
  • tail() - 得到除了第一个元素之外的所有元素
  • last() - 得到最后一个元素
  • length() - 元素数量

    谓词/比较器(测试元素,返回布尔值)

  • equal()
  • greaterThan()
  • lessThan()

    列表转换:

  • map() ([x]) -> [y] - 获取列表x并对该列表中的每个元素应用转换,返回新列表y
  • reverse() ([1, 2, 3]) -> [3, 2, 1]

    列表搜索:

  • find() ([x]) -> x - 获取列表x并返回匹配谓词的第一个元素
  • filter() ([x]) -> [y] - 取列表x并返回匹配谓词的所有元素

    列表归约器/折叠:

  • reduce() — ([x], function[, accumulator]) - 将函数应用于每个元素并将结果累积为单个值
  • any() - 如果任何值与谓词匹配,则为true

    迭代器/发生器/收集器(无限列表)

  • sample() - 返回连续输入源的当前值(温度,表格输入,切换开关状态等)
  • repeat() — (1) -> [1, 1, 1, 1, 1, 1,…]
  • cycle() / loop()  - 到达列表末尾时,再次回到开头。

其中一些实用工具程序已经添加到采用ECMAScript 5 Array中。

1
2
3
4
5
// Using ES6 syntax. () => means function () {}
var foo = [1, 2, 3, 4, 5];
var bar = foo.map( (n) => n + 1 ); // [2, 3, 4, 5, 6]
var baz = bar.filter( (n) => n >=3 && n <=5); // [3,4,5]
var bif = baz.reduce( (n, el) => n + el); // 12

作为泛型接口的列表

您可能会注意到上面的大多数函数都是用于管理或推导列表的实用程序。 一旦开始进行大量的函数式编程,您可能会开始将所有内容视为列表,列表元素,用于测试列表值的谓词或基于列表值的转换。 这种想法为极其可重用的代码奠定了基础。

泛型是能够处理各种不同数据类型的函数。 在JavaScript中,许多对集合进行操作的函数都是通用的。 例如,您可以对字符串和数组使用相同的函数,因为字符串可以视为字符数组。

将仅适用于单一类型的函数更改为适用于多种类型的函数的过程称为提升。

所有这些函数能处理你提供的任意类型数据的关键是,数据在列表中处理,且这些列表共享相同的接口。

如何停止微观管理一切

在面向对象和命令式编程中,我们对所有内容进行微观管理。 我们微观管理状态,迭代计数器,使用事件发射器和回调等事件时发生的时间。 如果我告诉你所有这些工作都是完全没必要的 - 你可以从程序中删除所有类别的代码?

反应式编程使用map,filter和reduce等函数实用程序来创建和处理通过系统传播变化的数据流:因此,反应式。 输入x更改时,输出y会自动更新以作为响应。

在OO中,您可以设置一些对象(例如,表单输入),并将该对象转换为事件发射器,并设置一个事件侦听器,该事件侦听器会跳过某些环节并且可能在触发事件时产生一些输出。

使用反应式编程时,您会以更具说明性的方式指定数据依赖性,并且大部分繁重工作都会卸载到标准功能实用程序中,因此您不必一次又一次地重新发明轮子。

想象一下,每个列表都是一个流:数组是元素顺序中的值流。 表单输入可以是每次更改时采样的值流。 按钮是点击流。

A stream is just a list expressed over time.

在Rx(反应式扩展)属于术语中,您创建可观察流,然后使用一组功能实用程序(如上所述)处理这些流。

想象一下,您想要查看特定哈希标记的所有社交媒体订阅源。 您可以收集列表列表,按照收到的顺序合并它们,然后使用上面的实用程序功能的任意组合来处理它们。
这可能看起来像这样:

1
2
3
4
var messages = merge(twitter, fb, gplus, github)
messages.map(normalize) // convert to single message format
.filter(selectHashtag) // cherry pick messages that use the tag
.pipe(process.stdout) // stream output to stdout

更好的异步

你可能听说过promise。 promise是一个对象,它提供了一个标准接口,用于处理在使用promise时可用或不可用的值。 换句话说,promise包含了可能在将来解析的可能值。 通常,一个函数调用会返回一个可以解析未来值的promise:

1
fetchFutureStockPrices().then(becomeAMillionaire);

好吧,它不能完全像那样,因为.then()只会在股票价格变得可用时调用,所以当becomeAMillionaire最终运行时,“未来”股票价格将是现在的股票价格。

promise基本上是仅发出单个值(或拒绝)的流。 Observable可以替换代码中的promise,并提供您可能已经习惯使用Underscore或Lo-Dash等库的所有标准函数式实用程序。

现在是了解函数式编程,函数纯度,惰性求值等等的好处的好时机。 我保证在接下来的几年里你会听到更多有关这些主题的内容。

0%