在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

就到這裏吧, 原文下面還有一段引伸, 不過難度有點深(關於計算機的, 機器學習的一些描述), 我也沒懂(其實上面我也是勉強看懂了, 收益確實良多)... 就不翻譯了...

相關文章
相關標籤/搜索