首先,假定你对函数式编程有所涉猎。用不了多久你就能明白纯函数的概念。随着深刻了解,你会发现函数式程序员彷佛对纯函数很着迷。他们说:“纯函数让你推敲代码”,“纯函数不太可能引起一场热核战争”,“纯函数提供了引用透明性”。诸如此类。他们说的并无错,纯函数是个好东西。可是存在一个问题……javascript
纯函数是没有反作用的函数。[1] 但若是你了解编程,你就会知道反作用是关键。若是没法读取 𝜋 值,为何要在那么多地方计算它?为了把值打印出来,咱们须要写入 console 语句,发送到 printer,或其余能够被读取到的地方。若是数据库不能输入任何数据,那么它又有什么用呢?咱们须要从输入设备读取数据,经过网络请求信息。这其中任何一件事都不可能没有反作用。然而,函数式编程是创建在纯函数之上的。那么函数式程序员是如何完成任务的呢?html
简单来讲就是,作数学家作的事情:欺骗。前端
说他们欺骗吧,技术上又遵照规则。可是他们发现了这些规则中的漏洞,并加以利用。有两种主要的方法:java
依赖注入是咱们处理反作用的第一种方法。在这种方法中,将代码中的不纯的部分放入函数参数中,而后咱们就能够把它们看做是其余函数功能的一部分。为了解释个人意思,咱们来看看一些代码:node
// logSomething :: String -> ()
function logSomething(something) {
const dt = new Date().toIsoString();
console.log(`${dt}: ${something}`);
return something;
}
复制代码
logSomething()
函数有两个不纯的地方:它建立了一个 Date()
对象而且把它输出到控制台。所以,它不只执行了 IO 操做, 并且每次运行的时候都会给出不一样的结果。那么,如何使这个函数变纯?使用依赖注入,咱们以函数参数的形式接受不纯的部分,所以 logSomething()
函数接收三个参数,而不是一个参数:python
// logSomething: Date -> Console -> String -> ()
function logSomething(d, cnsl, something) {
const dt = d.toIsoString();
cnsl.log(`${dt}: ${something}`);
return something;
}
复制代码
而后调用它,咱们必须自行明确地传入不纯的部分:android
const something = "Curiouser and curiouser!";
const d = new Date();
logSomething(d, console, something);
// ⦘ Curiouser and curiouser!
复制代码
如今,你可能会想:“这样作有点傻逼。这样把问题变得更严重了,代码仍是和以前同样不纯”。你是对的。这彻底就是一个漏洞。ios
YouTube 视频连接:youtu.be/9ZSoJDUD_bUgit
这就像是在装傻:“噢!不!警官,我不知道在 cnsl
上调用 log()
会执行 IO 操做。这是别人传给个人。我不知道它从哪来的”,这看起来有点蹩脚。程序员
这并不像表面上那么愚蠢,注意咱们的 logSomething()
函数。若是你要处理一些不纯的事情, 你就不得不把它变得不纯。咱们能够简单地传入不一样的参数:
const d = {toISOString: () => "1865-11-26T16:00:00.000Z"};
const cnsl = {
log: () => {
// do nothing
}
};
logSomething(d, cnsl, "Off with their heads!");
// ← "Off with their heads!"
复制代码
如今,咱们的函数什么事情也没干,除了返回 something
参数。可是它是纯的。若是你用相同的参数调用它,它每次都会返回相同的结果。这才是重点。为了使它变得不纯,咱们必须采起深思熟虑的行动。或者换句话说,函数依赖于右边的签名。函数没法访问到像 console
或者 Date
之类的全局变量。这样全部事情就很明确了。
一样须要注意的是,咱们也能够将函数传递给原来不纯的函数。让咱们看一下另外一个例子。假设表单中有一个 username
字段。咱们想要从表单中取到它的值:
// getUserNameFromDOM :: () -> String
function getUserNameFromDOM() {
return document.querySelector("#username").value;
}
const username = getUserNameFromDOM();
username;
// ← "mhatter"
复制代码
在这个例子中,咱们尝试去从 DOM 中查询信息。这是不纯的,由于 document
是一个随时可能改变的全局变量。把咱们的函数转化为纯函数的方法之一就是把 全局 document
对象看成一个参数传入。可是咱们也能够像这样传入一个 querySelector()
函数:
// getUserNameFromDOM :: (String -> Element) -> String
function getUserNameFromDOM($) {
return $("#username").value;
}
// qs :: String -> Element
const qs = document.querySelector.bind(document);
const username = getUserNameFromDOM(qs);
username;
// ← "mhatter"
复制代码
如今,你可能仍是会认为:“这样仍是同样傻啊!” 咱们所作只是把不纯的代码从 getUsernameFromDOM()
移出来而已。它并无消失,咱们只是把它放在了另外一个函数 qs()
中。除了使代码更长以外,它彷佛没什么做用。咱们两个函数取代了以前一个不纯的函数,可是其中一个仍然不纯。
别着急,假设咱们想给 getUserNameFromDOM()
写测试。如今,比较一下不纯和纯的版本,哪一个更容易编写测试?为了对不纯版本的函数进行测试,咱们须要一个全局 document
对象,除此以外,还须要一个 ID 为 username
的元素。若是我想在浏览器以外测试它,那么我必须导入诸如 JSDOM 或无头浏览器之类的东西。这一切都是为了测试一个很小的函数。可是使用第二个版本的函数,我能够这样作:
const qsStub = () => ({value: "mhatter"});
const username = getUserNameFromDOM(qsStub);
assert.strictEqual("mhatter", username, `Expected username to be ${username}`);
复制代码
如今,这并不意味着你不该该建立在真正的浏览器中运行的集成测试。(或者,至少是像 JSDOM 这样的模拟版本)。可是这个例子所展现的是 getUserNameFromDOM()
如今是彻底可预测的。若是咱们传递给它 qsStub 它老是会返回 mhatter
。咱们把不可预测转性移到了更小的函数 qs 中。
若是咱们这样作,就能够把这种不可预测性推得愈来愈远。最终,咱们将它们推到代码的边界。所以,咱们最终获得了一个由不纯代码组成的薄壳,它包围着一个测试友好的、可预测的核心。当您开始构建更大的应用程序时,这种可预测性就会起到很大的做用。
能够以这种方式建立大型、复杂的应用程序。我知道是 由于我作过。 依赖注入使测试变得更容易,也会使每一个函数的依赖关系变得明确。但它也有一些缺点。最主要的一点是,你最终会获得相似这样冗长的函数签名:
function app(doc, con, ftch, store, config, ga, d, random) {
// 这里是应用程序代码
}
app(document, console, fetch, store, config, ga, new Date(), Math.random);
复制代码
这还不算太糟,除此以外你可能遇到参数钻井的问题。在一个底层的函数中,你可能须要这些参数中的一个。所以,您必须经过许多层的函数调用来链接参数。这让人恼火。例如,您可能须要经过 5 层中间函数传递日期。全部这些中间函数都不使用 date 对象。这不是世界末日,至少可以看到这些显式的依赖关系仍是不错的。但它仍然让人恼火。这还有另外一种方法……
让咱们看看函数式程序员利用的第二个漏洞。它像这样:“发生的反作用才是反作用”。我知道这听起来神秘的。让咱们试着让它更明确一点。思考一下这段代码:
// fZero :: () -> Number
function fZero() {
console.log("Launching nuclear missiles");
// 这里是发射核弹的代码
return 0;
}
复制代码
我知道这是个愚蠢的例子。若是咱们想在代码中有一个 0,咱们能够直接写出来。我知道你,文雅的读者,永远不会用 JavaScript 写控制核武器的代码。但它有助于说明这一点。这显然是不纯的代码。由于它输出日志到控制台,也可能开始热核战争。假设咱们想要 0。假设咱们想要计算导弹发射后的状况,咱们可能须要启动倒计时之类的东西。在这种状况下,提早计划如何进行计算是彻底合理的。咱们会很是当心这些导弹何时起飞,咱们不想搞混咱们的计算结果,以避免他们意外发射导弹。那么,若是咱们将 fZero()
包装在另外一个只返回它的函数中呢?有点像安全包装。
// fZero :: () -> Number
function fZero() {
console.log("Launching nuclear missiles");
// 这里是发射核弹的代码
return 0;
}
// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
return fZero;
}
复制代码
我能够运行 returnZeroFunc()
任意次,只要不调用返回值,我理论上就是安全的。个人代码不会发射任何核弹。
const zeroFunc1 = returnZeroFunc();
const zeroFunc2 = returnZeroFunc();
const zeroFunc3 = returnZeroFunc();
// 没有发射核弹。
复制代码
如今,让咱们更正式地定义纯函数。而后,咱们能够更详细地检查咱们的 returnZeroFunc()
函数。若是一个函数知足如下条件就能够称之为纯函数:
让咱们看看 returnZeroFunc()
。有反作用吗?嗯,以前咱们肯定过,调用 returnZeroFunc()
不会发射任何核导弹。除非执行调用返回函数的额外步骤,不然什么也不会发生。因此,这个函数没有反作用。
returnZeroFunc()
引用透明吗?也就是说,给定相同的输入,它老是返回相同的输出?好吧,按照它目前的编写方式,咱们能够测试它:
zeroFunc1 === zeroFunc2; // true
zeroFunc2 === zeroFunc3; // true
复制代码
但它还不能算纯。returnZeroFunc()
函数引用函数做用域外的一个变量。为了解决这个问题,咱们能够以这种方式进行重写:
// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
function fZero() {
console.log("Launching nuclear missiles");
// 这里是发射核弹的代码
return 0;
}
return fZero;
}
复制代码
如今咱们的函数是纯函数了。可是,JavaScript 阻碍了咱们。咱们没法再使用 ===
来验证引用透明性。这是由于 returnZeroFunc()
老是返回一个新的函数引用。可是你能够经过审查代码来检查引用透明。returnZeroFunc()
函数每次除了返回相同的函数其余什么也不作。
这是一个巧妙的小漏洞。但咱们真的能把它用在真正的代码上吗?答案是确定的。但在咱们讨论如何在实践中实现它以前,先放到一边。先回到危险的 fZero()
函数:
// fZero :: () -> Number
function fZero() {
console.log("Launching nuclear missiles");
// 这里是发射核弹的代码
return 0;
}
复制代码
让咱们尝试使用 fZero()
返回的零,但这不会发动热核战争(笑)。咱们将建立一个函数,它接受 fZero()
最终返回的 0,并在此基础上加一:
// fIncrement :: (() -> Number) -> Number
function fIncrement(f) {
return f() + 1;
}
fIncrement(fZero);
// ⦘ 发射导弹
// ← 1
复制代码
哎呦!咱们意外地发动了热核战争。让咱们再试一次。这一次,咱们不会返回一个数字。相反,咱们将返回一个最终返回一个数字的函数:
// fIncrement :: (() -> Number) -> (() -> Number)
function fIncrement(f) {
return () => f() + 1;
}
fIncrement(zero);
// ← [Function]
复制代码
唷!危机避免了。让咱们继续。有了这两个函数,咱们能够建立一系列的 '最终数字'(译者注:最终数字即返回数字的函数,后面屡次出现):
const fOne = fIncrement(zero);
const fTwo = fIncrement(one);
const fThree = fIncrement(two);
// 等等…
复制代码
咱们也能够建立一组 f*()
函数来处理最终值:
// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number)
function fMultiply(a, b) {
return () => a() * b();
}
// fPow :: (() -> Number) -> (() -> Number) -> (() -> Number)
function fPow(a, b) {
return () => Math.pow(a(), b());
}
// fSqrt :: (() -> Number) -> (() -> Number)
function fSqrt(x) {
return () => Math.sqrt(x());
}
const fFour = fPow(fTwo, fTwo);
const fEight = fMultiply(fFour, fTwo);
const fTwentySeven = fPow(fThree, fThree);
const fNine = fSqrt(fTwentySeven);
// 没有控制台日志或热核战争。干得不错!
复制代码
看到咱们作了什么了吗?若是能用普通数字来作的,那么咱们也能够用最终数字。数学称之为 同构。咱们老是能够把一个普通的数放在一个函数中,将其变成一个最终数字。咱们能够经过调用这个函数获得最终的数字。换句话说,咱们创建一个数字和最终数字之间映射。这比听起来更使人兴奋。我保证,咱们很快就会回到这个问题上。
这样进行函数包装是合法的策略。咱们能够一直躲在函数后面,想躲多久就躲多久。只要咱们不调用这些函数,它们理论上都是纯的。世界和平。在常规(非核)代码中,咱们实际上最终但愿获得那些反作用可以运行。将全部东西包装在一个函数中可让咱们精确地控制这些效果。咱们决定这些反作用发生的确切时间。可是,输入那些括号很痛苦。建立每一个函数的新版本很烦人。咱们在语言中内置了一些很是好的函数,好比 Math.sqrt()
。若是有一种方法能够用延迟值来使用这些普通函数就行了。进入下一节 Effect 函子。
就目的而言,Effect 函子只不过是一个被置入延迟函数的对象。咱们想把 fZero
函数置入到一个 Effect 对象中。可是,在这样作以前,先把难度下降一个等级
// zero :: () -> Number
function fZero() {
console.log("Starting with nothing");
// 绝对不会在这里发动核打击。
// 可是这个函数仍然不纯
return 0;
}
复制代码
如今咱们建立一个返回 Effect 对象的构造函数
// Effect :: Function -> Effect
function Effect(f) {
return {};
}
复制代码
到目前为止,尚未什么可看的。让咱们作一些有用的事情。咱们但愿配合 Effetct 使用常规的 fZero()
函数。咱们将编写一个接收常规函数并延后返回值的方法,它运行时不触发任何效果。咱们称之为 map
。这是由于它在常规函数和 Effect 函数之间建立了一个映射。它可能看起来像这样:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x => g(f(x)));
}
};
}
复制代码
如今,若是你观察仔细的话,你可能想知道 map()
的做用。它看起来像是组合。咱们稍后会讲到。如今,让咱们尝试一下:
const zero = Effect(fZero);
const increment = x => x + 1; // 一个普通的函数。
const one = zero.map(increment);
复制代码
嗯。咱们并无看到发生了什么。让咱们修改一下 Effect,这样咱们就有了办法来“扣动扳机”。能够这样写:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x => g(f(x)));
},
runEffects(x) {
return f(x);
}
};
}
const zero = Effect(fZero);
const increment = x => x + 1; // 只是一个普通的函数
const one = zero.map(increment);
one.runEffects();
// ⦘ 什么也没启动
// ← 1
复制代码
而且只要咱们愿意, 咱们能够一直调用 map
函数:
const double = x => x * 2;
const cube = x => Math.pow(x, 3);
const eight = Effect(fZero)
.map(increment)
.map(double)
.map(cube);
eight.runEffects();
// ⦘ 什么也没启动
// ← 8
复制代码
从这里开始变得有意思了。咱们称这为函子,这意味着 Effect 有一个 map
函数,它 遵循一些规则。这些规则并不意味着你不能这样作。它们是你的行为准则。它们更像是优先级。由于 Effect 是函子你们庭的一份子,因此它能够作一些事情,其中一个叫作“合成规则”。它长这样:
若是咱们有一个 Effect e
, 两个函数 f
和 g
那么 e.map(g).map(f)
等同于 e.map(x => f(g(x)))
。
换句话说,一行写两个 map
函数等同于组合这两个函数。也就是说 Effect 能够这样写(回顾一下上面的例子):
const incDoubleCube = x => cube(double(increment(x)));
// 若是你使用像 Ramda 或者 lodash/fp 之类的库,咱们也能够这样写:
// const incDoubleCube = compose(cube, double, increment);
const eight = Effect(fZero).map(incDoubleCube);
复制代码
当咱们这样作的时候,咱们能够确认会获得与三重 map
版本相同的结果。咱们可使用它重构代码,并确信代码不会崩溃。在某些状况下,咱们甚至能够经过在不一样方法之间进行交换来改进性能。
但这些例子已经足够了,让咱们开始实战吧。
咱们的 Effect 构造函数接受一个函数做为它的参数。这很方便,由于大多数咱们想要延迟的反作用也是函数。例如,Math.random()
和 console.log()
都是这种类型的东西。但有时咱们想把一个普通的旧值压缩成一个 Effect。例如,假设咱们在浏览器的 window
全局对象中附加了某种配置对象。咱们想要获得一个 a 的值,但这不是一个纯粹的运算。咱们能够写一个小的简写,使这个任务更容易:[3]
// of :: a -> Effect a
Effect.of = function of(val) {
return Effect(() => val);
};
复制代码
为了说明这可能会很方便,假设咱们正在处理一个 web 应用。这个应用有一些标准特性,好比文章列表和用户简介。可是在 HTML 中,这些组件针对不一样的客户进行展现。由于咱们是聪明的工程师,因此咱们决定将他们的位置存储在一个全局配置对象中,这样咱们总能找到它们。例如:
window.myAppConf = {
selectors: {
"user-bio": ".userbio",
"article-list": "#articles",
"user-name": ".userfullname"
},
templates: {
greet: "Pleased to meet you, {name}",
notify: "You have {n} alerts"
}
};
复制代码
如今使用 Effect.of()
,咱们能够很快地把咱们想要的值包装进一个 Effect 容器, 就像这样
const win = Effect.of(window);
userBioLocator = win.map(x => x.myAppConf.selectors["user-bio"]);
// ← Effect('.userbio')
复制代码
映射 Effect 可能对咱们大有帮助。可是有时候咱们会遇到映射的函数也返回一个 Effect 的状况。咱们已经定义了一个 getElementLocator()
,它返回一个包含字符串的 Effect。若是咱们真的想要拿到 DOM 元素,咱们须要调用另一个非纯函数 document.querySelector()
。因此咱们可能会经过返回一个 Effect 来纯化它:
// $ :: String -> Effect DOMElement
function $(selector) {
return Effect.of(document.querySelector(s));
}
复制代码
如今若是想把它两放一块儿,咱们能够尝试使用 map()
:
const userBio = userBioLocator.map($);
// ← Effect(Effect(<div>))
复制代码
想要真正运做起来还有点尴尬。若是咱们想要访问那个 div,咱们必须用一个函数来映射咱们想要作的事情。例如,若是咱们想要获得 innerHTML
,它看起来是这样的:
const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
// ← Effect(Effect('<h2>User Biography</h2>'))
复制代码
让咱们试着分解。咱们会回到 userBio
,而后继续。这有点乏味,但咱们想弄清楚这里发生了什么。咱们使用的标记 Effect('user-bio')
有点误导人。若是咱们把它写成代码,它看起来更像这样:
Effect(() => ".userbio");
复制代码
但这也不许确。咱们真正作的是:
Effect(() => window.myAppConf.selectors["user-bio"]);
复制代码
如今,当咱们进行映射时,它就至关于将内部函数与另外一个函数组合(正如咱们在上面看到的)。因此当咱们用 $
映射时,它看起来像这样:
Effect(() => window.myAppConf.selectors["user-bio"]);
复制代码
把它展开获得:
Effect(
() => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio'])))
);
复制代码
展开 Effect.of
给咱们一个更清晰的概览:
Effect(() =>
Effect(() => document.querySelector(window.myAppConf.selectors["user-bio"]))
);
复制代码
注意: 全部实际执行操做的代码都在最里面的函数中,这些都没有泄露到外部的 Effect。
为何要这样拼写呢?咱们想要这些内嵌的 Effect 变成非内嵌的形式。转换过程当中,要保证没有引入任何预料以外的反作用。对于 Effect 而言, 不内嵌的方式就是在外部函数调用 .runEffects()
。 但这可能会让人困惑。咱们已经完成了整个练习,以检查咱们不会运行任何 Effect。咱们会建立另外一个函数作一样的事情,并将其命名为 join
。咱们使用 join
来解决 Effect 内嵌的问题,使用 runEffects()
真正运行全部 Effect。 即便运行的代码是相同的,但这会使咱们的意图更加清晰。
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x => g(f(x)));
},
runEffects(x) {
return f(x);
}
join(x) {
return f(x);
}
}
}
复制代码
而后,能够用它解开内嵌的用户简介元素:
const userBioHTML = Effect.of(window)
.map(x => x.myAppConf.selectors["user-bio"])
.map($)
.join()
.map(x => x.innerHTML);
// ← Effect('<h2>User Biography</h2>')
复制代码
.map()
以后紧跟 .join()
这种模式常常出现。事实上,有一个简写函数是很方便的。这样,不管什么时候咱们有一个返回 Effect 的函数,咱们均可以使用这个简写函数。它能够把咱们从一遍又一遍地写 map
而后紧跟 join
中解救出来。咱们这样写:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x => g(f(x)));
},
runEffects(x) {
return f(x);
}
join(x) {
return f(x);
}
chain(g) {
return Effect(f).map(g).join();
}
}
}
复制代码
咱们调用新的函数 chain()
由于它容许咱们把 Effect 连接到一块儿。(其实也是由于标准告诉咱们能够这样调用它)。[4] 取到用户简介元素的 innerHTML
可能长这样:
const userBioHTML = Effect.of(window)
.map(x => x.myAppConf.selectors["user-bio"])
.chain($)
.map(x => x.innerHTML);
// ← Effect('<h2>User Biography</h2>')
复制代码
不幸的是, 对于这个实现其余函数式语言有着一些不一样的名字。若是你读到它,你可能会有点疑惑。有时候它被称之为 flatMap
,这样起名是说得通的,由于咱们先进行一个普通的映射,而后使用 .join()
扁平化结果。不过在 Haskell 中,chain
被赋予了一个使人疑惑的名字 bind
。因此若是你在其余地方读到的话,记住 chain
、flatMap
和 bind
实际上是同一律念的引用。
这是最后一个使用 Effect 有点尴尬的场景,咱们想要在一个函数中组合两个或者多个函子。例如,如何从 DOM 中拿到用户的名字?拿到名字后还要插入应用配置提供的模板里呢?所以,咱们可能有一个模板函数(注意咱们将建立一个科里化版本的函数)
// tpl :: String -> Object -> String
const tpl = curry(function tpl(pattern, data) {
return Object.keys(data).reduce(
(str, key) => str.replace(new RegExp(`{${key}}`, data[key]),
pattern
);
});
复制代码
一切都很正常,可是如今来获取咱们须要的数据:
const win = Effect.of(window);
const name = win.map(w => w.myAppConfig.selectors['user-name'])
.chain($)
.map(el => el.innerHTML)
.map(str => ({name: str});
// ← Effect({name: 'Mr. Hatter'});
const pattern = win.map(w => w.myAppConfig.templates('greeting'));
// ← Effect('Pleased to meet you, {name}');
复制代码
咱们已经有一个模板函数了。它接收一个字符串和一个对象而且返回一个字符串。可是咱们的字符串和对象(name
和 pattern
)已经包装到 Effect 里了。咱们所要作的就是提高咱们 tpl()
函数到更高的地方使得它能很好地与 Effect 工做。
让咱们看一下若是咱们在 pattern Effect 上用 map()
调用 tpl()
会发生什么:
pattern.map(tpl);
// ← Effect([Function])
复制代码
对照一下类型可能会使得事情更加清晰一点。map 的函数声明可能长这样:
_map :: Effect a ~> (a -> b) -> Effect b_
复制代码
这是模板函数的函数声明:
_tpl :: String -> Object -> String_
复制代码
所以,当咱们在 pattern
上调用 map
,咱们在 Effect 内部获得了一个偏应用函数(记住咱们科里化过 tpl
)。
_Effect (Object -> String)_
复制代码
如今咱们想从 pattern Effect 内部传递值,但咱们尚未办法作到。咱们将编写另外一个 Effect 方法(称为 ap()
)来处理这个问题:
// Effect :: Function -> Effect
function Effect(f) {
return {
map(g) {
return Effect(x => g(f(x)));
},
runEffects(x) {
return f(x);
}
join(x) {
return f(x);
}
chain(g) {
return Effect(f).map(g).join();
}
ap(eff) {
// 若是有人调用了 ap,咱们假定 eff 里面有一个函数而不是一个值。
// 咱们将用 map 来进入 eff 内部, 而且访问那个函数
// 拿到 g 后,就传入 f() 的返回值
return eff.map(g => g(f()));
}
}
}
复制代码
有了它,咱们能够运行 .ap()
来应用咱们的模板函数:
const win = Effect.of(window);
const name = win
.map(w => w.myAppConfig.selectors["user-name"])
.chain($)
.map(el => el.innerHTML)
.map(str => ({ name: str }));
const pattern = win.map(w => w.myAppConfig.templates("greeting"));
const greeting = name.ap(pattern.map(tpl));
// ← Effect('Pleased to meet you, Mr Hatter')
复制代码
咱们已经实现咱们的目标。但有一点我要认可,我发现 ap()
有时会让人感到困惑。很难记住我必须先映射函数,而后再运行 ap()
。而后我可能会忘了参数的顺序。可是有一种方法能够解决这个问题。大多数时候,我想作的是把一个普通函数提高到应用程序的世界。也就是说,我已经有了简单的函数,我想让它们与具备 .ap()
方法的 Effect 一块儿工做。咱们能够写一个函数来作这个:
// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c)
const liftA2 = curry(function liftA2(f, x, y) {
return y.ap(x.map(f));
// 咱们也能够这样写:
// return x.map(f).chain(g => y.map(g));
});
复制代码
咱们称它为 liftA2()
由于它会提高一个接受两个参数的函数. 咱们能够写一个与之类似的 liftA3()
,像这样:
// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d)
const liftA3 = curry(function liftA3(f, a, b, c) {
return c.ap(b.ap(a.map(f)));
});
复制代码
注意,liftA2
和 liftA3
历来没有提到 Effect。理论上,它们能够与任何具备兼容 ap()
方法的对象一块儿工做。 使用 liftA2()
咱们能够像下面这样重写以前的例子:
const win = Effect.of(window);
const user = win.map(w => w.myAppConfig.selectors['user-name'])
.chain($)
.map(el => el.innerHTML)
.map(str => ({name: str});
const pattern = win.map(w => w.myAppConfig.templates['greeting']);
const greeting = liftA2(tpl)(pattern, user);
// ← Effect('Pleased to meet you, Mr Hatter')
复制代码
这时候你可能会想:“这彷佛为了不随处可见的奇怪的反作用而付出了不少努力”。这有什么关系?传入参数到 Effect 内部,封装 ap()
彷佛是一项艰巨的工做。当不纯代码正常工做时,为何还要烦恼呢?在实际场景中,你何时会须要这个?
函数式程序员听起来很像是中世纪的僧侣似的,他们禁绝了尘世中的种种乐趣而且指望这能使本身变得高洁。
—John Hughes [5]
让咱们把这些反对意见分红两个问题:
函数纯度的确重要。当你单独观察一个小函数时,一点点的反作用并不重要。写 const pattern = window.myAppConfig.templates['greeting'];
比写下面这样的代码更加快速简单。
const pattern = Effect.of(window).map(w => w.myAppConfig.templates("greeting"));
复制代码
若是代码里都是这样的小函数,那么继续这么写也能够,反作用不足以成问题。但这只是应用程序中的一行代码,其中可能包含数千甚至数百万行代码。当你试图弄清楚为何你的应用程序莫名其妙地“看似毫无道理地”中止工做时,函数纯度就变得更加剧要了。若是发生了一些意想不到的事,你试图把问题分解开来,找出缘由。在这种状况下,能够排除的代码越多越好。若是您的函数是纯的,那么您能够确信,影响它们行为的惟一因素是传递给它的输入。这就大大缩小了要考虑的异常范围。换句话说,它能让你少思考。这在大型、复杂的应用程序中尤其重要。
好吧。若是你正在构建一个大型的、复杂的应用程序,相似 Facebook 或 Gmail。那么函数纯度可能很重要。但若是不是大型应用呢?让咱们考虑一个愈加广泛的场景。你有一些数据。不仅是一点点数据,而是大量的数据 —— 数百万行,在 CSV 文本文件或大型数据库表中。你的任务是处理这些数据。也许你在训练一我的工神经网络来创建一个推理模型。也许你正试图找出加密货币的下一个大动向。不管如何, 问题是要完成这项工做须要大量的处理工做。
Joel Spolsky 使人信服地论证过 函数式编程能够帮助咱们解决这个问题。咱们能够编写并行运行的 map
和 reduce
的替代版本,而函数纯度使这成为可能。但这并非故事的结尾。固然,您能够编写一些奇特的并行处理代码。但即使如此,您的开发机器仍然只有 4 个内核(若是幸运的话,多是 8 个或 16 个)。那项工做仍然须要很长时间。除非,也就是说,你能够在一堆处理器上运行它,好比 GPU,或者整个处理服务器集群。
要使其工做,您须要描述您想要运行的计算。可是,您须要在不实际运行它们的状况下描述它们。听起来是否是很熟悉?理想状况下,您应该将描述传递给某种框架。该框架将当心地负责读取全部数据,并将其在处理节点之间分割。而后框架会把结果收集在一块儿,告诉你它的运行状况。这就是 TensorFlow 的工做流程。
TensorFlow™ 是一个高性能数值计算开源软件库。它灵活的架构支持从桌面到服务器集群,从移动设备到边缘设备的跨平台(CPU、GPU、TPU)计算部署。Google AI 组织内的 Google Brain 小组的研究员和工程师最初开发 TensorFlow 用于支持机器学习和深度学习领域,其灵活的数值计算内核也应用于其余科学领域。
—TensorFlow 首页[6]
当您使用 TensorFlow 时,你不会使用你所使用的编程语言中的常规数据类型。而是,你须要建立张量。若是咱们想加两个数字,它看起来是这样的:
node1 = tf.constant(3.0, tf.float32)
node2 = tf.constant(4.0, tf.float32)
node3 = tf.add(node1, node2)
复制代码
上面的代码是用 Python 编写的,可是它看起来和 JavaScript 没有太大的区别,不是吗?和咱们的 Effect 相似,add
直到咱们调用它才会运行(在这个例子中使用了 sess.run()
):
print("node3: ", node3)
print("sess.run(node3): ", sess.run(node3))
#⦘ node3: Tensor("Add_2:0", shape=(), dtype=float32)
#⦘ sess.run(node3): 7.0
复制代码
在调用 sess.run()
以前,咱们不会获得 7.0。正如你看到的,它和延时函数很像。咱们提早计划好了计算。而后,一旦准备好了,发动战争。
本文涉及了不少内容,可是咱们已经探索了两种方法来处理代码中的函数纯度:
依赖注入的工做原理是将代码的不纯部分移出函数。因此你必须把它们做为参数传递进来。相比之下,Effect 函子的工做原理则是将全部内容包装在一个函数后面。要运行这些 Effect,咱们必须先运行包装器函数。
这两种方法都是欺骗。他们不会彻底去除不纯,他们只是把它们推到代码的边缘。但这是件好事。它明确说明了代码的哪些部分是不纯的。在调试复杂代码库中的问题时,颇有优点。
这不是一个完整的定义,但暂时可使用。咱们稍后会回到正式的定义。 ↩
在其余语言(如 Haskell)中,这称为 IO 函子或 IO 单子。PureScript 使用 Effect 做为术语。我发现它更具备描述性。 ↩
注意,不一样的语言对这个简写有不一样的名称。例如,在 Haskell 中,它被称为 pure
。我不知道为何。 ↩
在这个例子中,采用了 Fantasy Land specification for Chain 规范。 ↩
John Hughes, 1990, ‘Why Functional Programming Matters’, Research Topics in Functional Programming ed. D. Turner, Addison–Wesley, pp 17–42, www.cs.kent.ac.uk/people/staf… ↩
TensorFlow™:面向全部人的开源机器学习框架, www.tensorflow.org/,12 May 2018。 ↩
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。