首先,假定你對函數式編程有所涉獵。用不了多久你就能明白純函數的概念。隨着深刻了解,你會發現函數式程序員彷佛對純函數很着迷。他們說:「純函數讓你推敲代碼」,「純函數不太可能引起一場熱核戰爭」,「純函數提供了引用透明性」。諸如此類。他們說的並無錯,純函數是個好東西。可是存在一個問題……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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。