在javascript中使用纯函数处理反作用

在javascript中使用纯函数处理反作用

今天给你们带来一片译文, 详情请点击这里.可能在墙内哦javascript

开始了, 若是你点开这篇文章, 就证实你已经开始涉及函数式编程了, 这距离你知道纯函数的概念不会好久. 若是你继续下去, 你就知道纯函数是真重要, 没有它你将步履维艰.你可能听过这样的话: "纯函数让你理清你的代码", "纯函数不可能会引发反作用","纯函数式引用透明的". 这些话没错, 纯函数是个好东西, 不过它仍是存在着一些问题...java

纯函数的概念

一个纯函数是一个没有反作用的函数, 可是若是你对编程有所了解, 你就知道反作用是不可避免的. 咱们把π计算到一百位可是又没有人能去记住它(最强大脑就忽略不计了) 因此咱们要在某个地方把他打印出来, 咱们就须要写一个 console 或者把数据传入打印机, 或者把他给某个能展现他的东西; 咱们要把数据放进数据库, 咱们须要阅读输入设备的数据, 须要从网络上获取信息, 这些都是反作用. 可是呢, 函数式编程主要靠的又是纯函数, 那么咱们如何去用函数式编程去管理反作用呢?git

简单的回答是: 干数学家干的事情(障眼法) 表面上, 数学家们技术上沿着规则来进行研究, 可是他们又会从那些规则中找到一个漏洞, 而且会奋力的把这些漏洞放大到能让一只大象走过去.程序员

有两个主要的漏洞来干这个事情github

  1. 依赖注入 dependency injection
  2. 使用 effect functor (名词: 只能意会, 不能言传)

依赖注入

依赖注入是咱们处理反作用的第一种方法, 在这个例子中, 咱们在代码中引入一些不纯的东西,好比, 打印一个字符串;web

// logSomething :: String -> String 
// 上面是函数的一种描述, logSomething 是函数的名字, 后面一个是参数类型, 一个是返回值类型
function logSomething(something) {
    const dt = (new Date())toISOString();
    console.log(`${dt}: ${something}`);
    return something;
}
复制代码

咱们的 logSomething 有两个不纯的东西, 一个是咱们建立了一个 Date 实例, 一个是咱们执行了 console.log, 咱们不只仅是执行了 IO 操做, 并且每次执行这个函数, 都会有不一样的结果. 因此, 你要怎么把这个函数变成纯函数呢? 用依赖注入, 咱们把不纯的源头都变成函数的参数, 因此, 咱们的这个函数须要用 3 个参数, 修改后的代码以下;数据库

// logSomething: Date -> Console -> String -> *
function logSomething(date, console, something) {
    const dt = date.toISOString();
    console.log(`${dt}: ${something}`);
    return something;
}
复制代码

而后咱们能够执行他了, 可是咱们必需要明确的知道哪个参数会引发反作用.编程

const something = "Curiouser and curiouser!"
const d = new Date();
logSomething(d, console, something);
复制代码

如今你可能会想, "这也太傻了吧! 咱们作的事情, 只是把问题转移到上一级, 咱们的这个函数依然是不纯的啊.... " yes that right 确实, 好像并无什么卵用.浏览器

这就比如伪装不知道: "oh 不 长官, 我彻底不知道在 console 上调用 log() 会引起 IO 操做, 这是别人传给个人一个参数, 我甚至都不知道这个对象是从哪里来的"; 这看起来有点蹩脚.安全

虽然是这样, 可是这并无看起来的那么愚蠢, 注意下咱们的 logSomething 函数中的下面这一点, 若是你想让他作一些不纯的东西, 你不得不让他是不纯的. 咱们只须要很简单的传入一个不一样的参数, 他就能够变成纯的.

const d = {toISOString: () => '1865-11-26T16:00:00.000Z'};
const cnsl = {
    log: () => {
        // do nothing
    },
};
logSomething(d, cnsl, "Off with their heads!");
复制代码

如今咱们的函数除了返回一个字符串, 什么东西也没作, 可是他是一个纯函数, 若是你使用一样的参数去调用这个函数, 它总会返回一样的值, 这就是重点. 要让它不纯, 咱们须要刻意的去作, 或者换一种方式去作, 这个函数的全部依赖都已经在参数中了, 他不会接受任何像 console, Date 这样的全局对象, 这让一切都很清晰.

还有一个很重要的点, 咱们能够传入一个函数到咱们原来不纯的函数, 让咱们来看另一个例子, 想象一下咱们在 form 表单的某处有一个 username, 咱们可能回这样取得它的值:

// getUserNameFromDOM :: () -> String
function getUserNameFromDOM() {
    return document.querySelector('#username').value;
}

const username = getUserNameFromDOM();
username;
复制代码

在这个例子中, 咱们尝试去检索 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;
复制代码

如今你可能仍是会想, 这仍是很傻啊, 咱们只是把不纯的代码从 getUserNameFromDom 转移了出去, 反作用还在啊, 它并无消失, 这看起来他除了让代码更长, 更多之外, 并无什么实质性的做用了, 本来咱们只有一个不纯的函数, 如今咱们有了两个函数, 其中一个仍是不纯的...

再忍受我一下, 想象一下咱们要给 getUserNameFromDOM() 写一个测试用例, 如今, 咱们来对比一下纯的版本和不纯的版本, 哪个更容易被测试? 为了让不纯的版本工做起来, 咱们须要一个 document 的全局对象, 最重要的是, 他还要有一个 id 是 username 的标签, 若是我在浏览器外对他进行测试, 那么我就须要引入 jsDOM 或者 无头浏览器(headless browser), 全部的这些都只是为了测试这么一个小小的函数, 可是若是使用纯的版本, 我能够直接这样来测试:

const qsStub = () => ({value: 'mhatter'});
const username = getUserNameFromDOM(qsStub);
// 断言 (接触测试框架比较少的小伙伴能够了解一下 jest)
assert.strictEqual('mhatter', username, `Expected username to be ${username}`);
复制代码

这不意味着你不该该在真是的浏览器上新建一个测试, 可是如今, getUserNameFromDOM() 是彻底的可预测的, 若是咱们老是传递 qsStub, 那么这个函数老是会返回 mhatter, 咱们把不可预测转移到了另一个函数 qs

若是咱们须要, 咱们能够把不可预测推到更远的函数上, 最终, 咱们会把他推到咱们代码的边缘, 对应于函数栈, 就是推到最后一个函数, 若是你要构建一个很大的 APP, 那么可预测性会变得愈加的重要.

依赖注入的缺点

构建一个庞大的, 复杂的应用是可能的, 由于原做者本身就搞了一个, 测试会变得更容易, 这使得全部的函数依赖都会明确, 可是这仍是有一些缺点, 最主要的一个就是, 要传递的参数真的太多了...

function app(doc, con, ftch, store, config, ga, d, random) {
    // Application code goes here
 }

app(document, console, fetch, store, config, ga, (new Date()), Math.random);
复制代码

也没有那么很差是吧... 可是若是你的参数有 穿针 (穿针名词解释: 在个人家乡,没有碰到篮筐就进的球, 叫穿针球)问题. 你可能在函数栈比较接近低的位置须要一些参数, 那么你就须要把这些参数一层一层的往下传, 这是很苦恼的, 例如, 你可能须要传递一些数据穿透5个中间函数, 而那些函数并无使用到这些数据, 这些数据只是为了第六个函数准备的(穿针), 这还不算严重, 毕竟他的依赖关系仍是很清晰,可是这仍是很苦恼, 然而, 咱们还有另一种方法来避免反作用

lazy functions (延迟函数)

让咱们来看一下函数式程序员使用的第二个漏洞, 这个漏洞是这样的: "当一个反作用尚未发生(执行)的时候, 他不是一个反作用." 听起来很神秘, 我也这么以为, 那让咱们来看看代码, 让他更清晰.

// fZero :: () -> Number
function fZero() {
    console.log('Launching nuclear missiles');
    // Code to launch nuclear missiles goes here (这里会执行一些反作用的代码, 发射火箭)
    return 0;
}
复制代码

我知道, 这是一个没有什么技术含量的例子, 若是咱们须要一个 0 咱们能够直接写一个 0 ...

可是咱们来举一个例子, 我知道咱们不可能用 javascript 来发射一支火箭, 可是这能够帮助咱们说明这个状况, 那么如今, 就让咱们用javascript 来发射一支火箭, 这是一段不纯的代码, 他打印了一条信息, 还发射了一支火箭. 想象咱们想获得那个0, 再想象一个场景,咱们想要在火箭发射以后要计算一些东西, 咱们可能须要启动一个计数器, 或者相似的东西, 咱们须要在火箭发射的时候很是的专一, 咱们不但愿火箭的意外发射会影响到咱们的计算, 那么, 若是咱们把 fZero() 包裹在另外一个仅仅只是返回它的函数上,会发生什么. 像是一种安全的包裹

// fZero :: () -> Number
function fZero() {
    console.log('Launching nuclear missiles');
    // Code to launch nuclear missiles goes here
    return 0;
}

// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
    return fZero;
}
复制代码

我能够执行 returnZeroFunc() 任意屡次, 只要我不执行他返回的函数, 那么咱们的火箭就不会发射, 理论上, 个人计算就是安全的.

const zeroFunc1 = returnZeroFunc();
const zeroFunc2 = returnZeroFunc();
const zeroFunc3 = returnZeroFunc();
// No nuclear missiles launched. 没有火箭发射
复制代码

如今, 咱们来定义一个纯函数, 而后更详细的审查 returnZeroFunc() 若是一个函数式纯的, 他有如下的两点特征

  1. 它没有反作用
  2. 引用透明, 意味着, 给它相同的参数, 它总会返回相同的值.

让咱们来审查一下 returnZeroFunc() 它有反作用吗? 它只是把函数返回了, 除非你继续执行它返回的函数, 否则它不会发射火箭, 因此在这, 它没有反作用.

那它引用透明吗? 传相同的参数, 会返回相同的值吗? oh 他老是返回同一个函数, 在这, 他引用是透明的, 咱们能够测试一下

zeroFunc1 === zeroFunc2; // true
zeroFunc2 === zeroFunc3; // true
复制代码

但, 它还不是彻底纯, 由于他引用了外部的一个变量, 可是咱们能够这样来改写它

// returnZeroFunc :: () -> (() -> Number)
function returnZeroFunc() {
    function fZero() {
        console.log('Launching nuclear missiles');
        // Code to launch nuclear missiles goes here
        return 0;
    }
    return fZero;
}
复制代码

如今咱们的函数纯了, 可是咱们每次返回的函数都不是同一个函数, 由于, 每一次执行都会从新定义一个 fZero, 这是 javascript 给咱们开的一个玩笑, 不过并无什么大碍

这是一个优雅的小漏洞, 可是, 咱们能够在实际的项目中使用这样的代码吗? 答案是确定的, 可是在咱们要把他引入实际代码以前, 咱们来把目标放得更长远一点, 回到咱们的不纯的 fZero() 函数.

// fZero :: () -> Number
function fZero() {
    console.log('Launching nuclear missiles');
    // Code to launch nuclear missiles goes here
    return 0;
}
复制代码

咱们来使用一下 fZero() 返回的 0 , 可是咱们不发射里面的火箭. 咱们会新建一个函数, 这个函数会携带着 fZero() 返回的 0, 而后给它加 1

// fIncrement :: (() -> Number) -> Number
function fIncrement(f) {
    return f() + 1;
}

fIncrement(fZero);
复制代码

卧槽, 咱们意外的发射了火箭... 咱们再来一次, 此次咱们不会返回一个数字(number), 我会回返回一个会返回数字的函数 这里其实就是上面的函数的安全包裹(延迟函数)

// fIncrement :: (() -> Number) -> (() -> Number)
function fIncrement(f) {
    return () => f() + 1;
}

fIncrement(fZero);
复制代码

oh, 火箭不发射了, 咱们继续, 有了这两个函数, 咱们能够构建一系列的函数区作咱们想要作的事情,.;

const fOne   = fIncrement(fZero);
const fTwo   = fIncrement(one);
const fThree = fIncrement(two);
// And so on…
复制代码

咱们还能够建立一推使用上面函数的函数来作一些更高级的事情.

// 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);
// No console log or thermonuclear war. Jolly good show!
复制代码

你知道我在这里都干了什么吗? 我想作的任何的事情, 都是从那个 fZero() 返回的0开始的, 我把全部的计算逻辑都写好了, 个人火箭仍是没有发射, 咱们能够经过调用最终的函数拿到咱们最后的值, 而且发射火箭, 这里有个数学理论, 叫'isomorphism', 感兴趣的能够去看一下.

若是这里还不是很明白, 咱们能够换一种说法, 咱们能够把这看成是咱们想要得到的那个数字跟 0 的一种映射关系, 咱们能够经过一系列的操做, 把 0 映射成咱们想要的那个值, 并且这个关系是一一对应的, 咱们经过这个操做, 只能获取得那个值, 由于都是纯函数. 这听起来很兴奋.

包裹着那些函数的函数是一个合法的策略, 只要咱们想, 咱们能够一直让函数隐藏在最后一步, 只要咱们不调用实际执行的函数, 他们理论上全是纯的, 并且不会发射任何的火箭. 在那些纯的代码中, 咱们最终仍是须要反作用的(否则怎么发射火箭), 把全部的一切都包裹在一个函数里面, 可让咱们精确的控制管理好那些反作用, 当那些反作用发生的时候, 咱们能够精确的知道他发生了, 可是, 声明那么多得函数管理起来是很痛苦的, 并且咱们还要为每个函数都建立一个被包裹的版本, 咱们须要一些像 Math.sqrt() 这样的 javascript 语言内置的完美的函数去干这个事情, 若是咱们有一种方式, 能够用咱们的延迟值去使用那些普通函数, 这就太好了. 如今让咱们来引入 effect functor (名词不翻译, 请你们意会)

the effect functor

从咱们的目的出发, effect functor 不过就是一个持有咱们的延迟函数的对象, 因此咱们会把 fZero() 放进去, 可是, 在咱们干这个事情以前, 咱们来看一个简单一点的例子.

// 这是咱们的延迟函数
// zero :: () -> Number
function fZero() {
    console.log('Starting with nothing');
    // Definitely not launching a nuclear strike here.
    // But this function is still impure.
    return 0;
}
复制代码

如今, 咱们来建立一个能够为咱们新建 effect 对象的构造器(工厂函数, 不用使用new)

// Effect :: Function -> Effect
function Effect(f) {
    return {};
}
复制代码

目前为止, 东西并很少, 任何 javascript 开发者都能看懂这个代码, 很简单, 如今, 咱们来搞一些有用的东西, 咱们如今把 Effect 跟咱们普通的 fZero() 函数一块儿使用, 咱们来写一个带着普通的函数的方法, 最终咱们会把他应用到咱们的延迟值上, 可是, 咱们不会触发 反作用 ,咱们把他叫作 map 这是由于, 他会在常规的函数和 Effect 函数之间创建一种映射的关系, 这看起来可能会是这样的.

// Effect :: Function -> Effect
function Effect(f) {
    return {
        map(g) {
            return Effect(g(f(x))
        } 
    };
}
复制代码

若是你不是函数式编程的新手, 并且你很专一的看到这里, 你可能会发现, 这跟 compose 很像, 咱们会在稍后来讲这个问题, 如今咱们来试试这样干

const zero = Effect(fZero);
const increment = x => x + 1; // A plain ol' regular function.
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; // Just a regular function.
const one = zero.map(increment);

one.runEffects();
// ⦘ Starting with nothing
// ← 1
复制代码

咱们能够一直调用 Effect 的 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();
// ⦘ Starting with nothing
// ← 8
复制代码

如今, 事情变得有趣了, 咱们把这个东西叫 functor, 全部有 map() 方法的 Effect 都是, 它有一些规则, 这些规则不是限制什么你不能作, 而是限制了什么你能够作的, 就像是特权, 由于 Effect 只是 functor 的一种, 他们其中的一个规则是 composition rule 他看起来像是这样的.

若是咱们有一个 Effect 叫 e 和两个函数叫 f 和 g 那么 e.map(g).map(f) 等于 e.map(f(g(x)))

换一种说法, 连续的执行两个 map 等于 合并两个函数,执行一次map 意味着, Effect 能够作这样作这个事情

const incDoubleCube = x => cube(double(increment(x)));
// If we're using a library like Ramda or lodash/fp we could also write:
// const incDoubleCube = compose(cube, double, increment);
const eight = Effect(fZero).map(incDoubleCube);
复制代码

当咱们这样作的时候, 咱们保证,这个和上面的 三个map 的版本获得的结果是同样的, 咱们能够用这个去重构咱们的代码并且不会破坏原有的代码, 咱们甚至能够经过交换两个函数的顺序来提高性能. 如今咱们再来作一些进阶.

建立 Effect 的捷径

咱们的 Effect 构造器带着一个函数作参数, 这很方便, 由于多数的反作用都是在一个函数中发生的, 例如, Math.random() console.log 和相似的这些函数, 要是咱们想在 Effect 中放一个 其余类型的值呢?(例如放一个 对象), 想象一下, 咱们须要在浏览器的 window 对象上绑定一个配置对象, 咱们想获取他的值, 可是, 它是一个全局的对象, 它可能在任什么时候候被修改, 这是反作用, 咱们能够写一个方法来让建立 Effect 的方式变得更丰富.

// 这是一个静态的方法
// of :: a -> Effect a
Effect.of = function of(val) {
    return Effect(() => val);
}
复制代码

为了让大家知道这是多么的有用, 想象一下咱们如今在一个 web app 上, 这个应用有一些固定的功能, 例如,文章列表 还有做者的信息, 可是, 这个应用的 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 里面

const win = Effect.of(window);
userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']);
// ← Effect('.userbio')
// 如今咱们获得的是一个 Effect 而后里面装着一个函数 () => '.userbio'
// 一会会回到这里继续讲解
复制代码

嵌套 Effect 和 扁平 Effect

Effect 的映射可让咱们走很长的路, 可是有的时候, 咱们会映射一个返回 Effect 的函数, 这就尴尬了. 好比, 咱们如今想真的找到上面的那个选择器的 DOM 节点, 咱们就须要另一个不纯的API document.querySelector() 噢 又是一个反作用, 因此咱们打算把他放进 一个返回 Effect 的函数中

// $ :: String -> Effect DOMElement
function $(selector) {
    return Effect.of(document.querySelector(s));
}
复制代码

如今, 若是咱们想把这个 $ 和上面的 userBioLocator 一块儿使用(他们为何要一块儿使用不用解释吧...), 咱们须要使用map

const userBio = userBioLocator.map($);
// ← Effect(Effect(<div>))
复制代码

到了这一步就有点尴尬了, 若是咱们想要访问那个 div 咱们就要继续 map一个函数, 而那个函数里面 还要再次 map 才能够获得咱们真正想要的值, (Effect 嵌套了) 若是咱们想要访问div的 innerHTML 那么代码多是这样的.

const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
// ← Effect(Effect('<h2>User Biography</h2>'))
复制代码

如今咱们来重新的捋一捋思路, 咱们回到 userBio 那一步, 会有点繁琐, 可是能让咱们更清晰的看看他是怎么嵌套的. 咱们上面的 Effect('.userbio') 这个描述可能有点迷惑, 它实际上是下面这样的

ps: 下面这个过程还有疑惑的请在掘金平台评论区回复, 我看见会回答.

Effect(() => '.userbio')
复制代码

其实咱们还能够继续的展开,

Effect(() => window.myAppConf.selectors['user-bio']);
复制代码

咱们的 map 至关于将 参数里的函数, 跟 Effect 内部保管的函数相合并, 因此当咱们传入一个 $ 的时候, 他就变成这个样子的

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

see 嵌套了, 然而, 反作用仍是保持在内部的Effect 它并无影响到外部的 Effect 能够说外部的 Effect 已经毫无做用了

Join

为何我要把它展开, 由于我想把这个 嵌套的 Effect 给扁平了, 让它变成一个 Effect , 并且是在不触发反作用的条件下把它扁平掉, 不知道你想到了没有, 咱们如今已经有一个方法能够获取到 Effect 的值了, 是的, 就是 runEffects, 咱们能够直接在外部的Effect 行执行 runEffects 就能够拿到内部的 Effect, 可是, 咱们的 runEffects 最初是用来执行咱们的延迟值函数的, 而延迟值会触发反作用, 那不是有歧义了吗, 由于咱们默认 runEffects 会触发反作用, 可是咱们的 扁平化 是不触发反作用的, 因此咱们须要新建另一个函数, 来干相同的事情.这会让咱们清楚的看函数调用就知道, 咱们实际干了啥. 即便, 这两个函数是如出一辙的

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

Chain

想上面的这种, map 完了之后立刻就 join 的写法会有不少, 这就好像是捆绑的操做, 因此咱们能够给他们来一个 快捷方式, 这让咱们能够很安全的一直 map jion 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).jion()
        }
    }
}
复制代码

咱们把这个函数叫 chain (具备链子的意思, 并且标准也规定了这个名字, 大家能够去查阅) 是由于他能够把两个 Effect 给链接在一块儿. 如今来看看咱们重构过的代码

const userBioHTML = Effect.of(window)
    .map(x => x.myAppConf.selectors['user-bio'])
    .chain($)
    .map(x => x.innerHTML);
// ← Effect('<h2>User Biography</h2>')
复制代码

不幸的是, 其余的函数式程序员, 会用他们本身的命名, 若是大家会阅读到他们的文章, 可能回对你形成一点困惑, 有时候他会叫 flatMap 熟悉吧, 我记得 rxjs 就是使用的这个, 还有一些库会使用 bind 因此当大家看到这些词的时候注意一下, 他们实际上是相同的概念.

结合 Effect

使用 Effect 的时候还有一个场景可能会比较尴尬, 就是咱们须要用一个函数去组合多个 Effect 的时候. 例如, 当咱们要在 DOM 节点中获取用户名而后把它插入到咱们配置的模板(template)中, 因此咱们须要一个这样的操做模板的函数

// 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
    );
});
// 不知道curry的都要去了解一下哦, 很重要的一个概念
复制代码

一切都很好, 如今来获取咱们的用户名了

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

咱们已经有模板函数了, 它须要两个参数, 一个字符串(模板), 一个对象(config对象), 而后返回一个字符串. 可是咱们的字符串和对象都包裹在 Effect 里面, 因此咱们只能在 Effect 内部传递参数给 tpl

如今咱们来看一下在 pattern 上把 tpl 函数传入 map 里面会发生什么

pattern.map(tpl)
// ← Effect([Function])
复制代码

别混乱啊, 这里要好好捋一捋, 咱们传入了 tpl 而tpl 是一个 curry 过的函数, 这个函数在接受到他的两个参数以前, 他是不会执行的, 也就是说 咱们的 pattern.map() 返回的 Effect 是一个包裹了 tpl('Pleased to meet you, {name}', ?) 的 Effect , 他还须要一个配置对象才会返回他真正想要返回的值.

如今, 咱们须要把config 对象传进 Effect 里面的那个 已经具备一个参数的 tpl 函数了, 可是咱们好像尚未方法去干这个事情, 咱们如今来建立一个(咱们把这个方法叫 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) {
             // If someone calls ap, we assume eff has a function inside it (rather than a value).
            // We'll use map to go inside off, and access that function (we'll call it 'g')
            // Once we've got g, we apply the value inside off f() to it
            // 咱们默认传进来的是一个 Effect 
            return eff.map(g => g(f()));
        }
    }
}
复制代码

好好的看一看这个函数, 好好理解一下, 这个很差解释, 展开了就懂了.

如今咱们可使用一下这个函数了

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() 有时候会有点尴尬, 就是很难去记住个人函数要 map 了一个参数才能够传进去 ap(), 若是我忘记了这个函数的参数的顺序那我就GG了, 这有一个方法, 大多时候, 咱们会把普通函数提高到全应用的级别, 就是, 我有一个普通的函数, 我想让它跟与一个拥有 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));
    // We could also write:
    // 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)));
});
复制代码

注意一下, 咱们这里并无涉及到 Effect 我刚才说了, 是一个与拥有 ap() 的 Efect 相似的东西, 这些函数能够与任何拥有 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')
复制代码

完了吗?

到了这里, 你可能会以为为了不这样或者那样的反作用, 咱们但是煞费苦心了. 可是这又怎么样呢, 当你意识到他的好处的时候, 这样点点的麻烦彻底OJBK

就到这里吧, 原文下面还有一段引伸, 不过难度有点深(关于计算机的, 机器学习的一些描述), 我也没懂(其实上面我也是勉强看懂了, 收益确实良多)... 就不翻译了...

相关文章
相关标签/搜索