摘要:函數(shù)式編程一般約定,函子有一個(gè)方法,用來(lái)生成新的容器。是實(shí)現(xiàn)了函數(shù)并遵守一些特定規(guī)則的容器類(lèi)型。定義二若為廣群,且運(yùn)算還滿足結(jié)合律,即任意,有,則稱為半群。
slide 地址
四、Talk is cheap!Show me the ... MONEY!以下內(nèi)容主要參考自 Professor Frisby Introduces Composable Functional JavaScript4.1.容器(Box)
假設(shè)有個(gè)函數(shù),可以接收一個(gè)來(lái)自用戶輸入的數(shù)字字符串。我們需要對(duì)其預(yù)處理一下,去除多余空格,將其轉(zhuǎn)換為數(shù)字并加一,最后返回該值對(duì)應(yīng)的字母。代碼大概長(zhǎng)這樣...
const nextCharForNumStr = (str) => String.fromCharCode(parseInt(str.trim()) + 1) nextCharForNumStr(" 64 ") // "A"
因缺思廳,這代碼嵌套的也太緊湊了,看多了“老闊疼”,趕緊重構(gòu)一把...
const nextCharForNumStr = (str) => { const trimmed = str.trim() const number = parseInt(trimmed) const nextNumber = number + 1 return String.fromCharCode(nextNumber) } nextCharForNumStr(" 64 ") // "A"
很顯然,經(jīng)過(guò)之前內(nèi)容的熏(xi)陶(nao),一眼就可以看出這個(gè)修訂版代碼很不 Pointfree...
為了這些只用一次的中間變量還要去想或者去查翻譯,也是容易“老闊疼”,再改再改~
const nextCharForNumStr = (str) => [str] .map(s => s.trim()) .map(s => parseInt(s)) .map(i => i + 1) .map(i => String.fromCharCode(i)) nextCharForNumStr(" 64 ") // ["A"]
這次借助數(shù)組的 map 方法,我們將必須的4個(gè)步驟拆分成了4個(gè)小函數(shù)。
這樣一來(lái)再也不用去想中間變量的名稱到底叫什么,而且每一步做的事情十分的清晰,一眼就可以看出這段代碼在干嘛。
我們將原本的字符串變量 str 放在數(shù)組中變成了 [str],這里就像放在一個(gè)容器里一樣。
代碼是不是感覺(jué)好 door~~ 了?
不過(guò)在這里我們可以更進(jìn)一步,讓我們來(lái)創(chuàng)建一個(gè)新的類(lèi)型 Box。我們將同樣定義 map 方法,讓其實(shí)現(xiàn)同樣的功能。
const Box = (x) => ({ map: f => Box(f(x)), // 返回容器為了鏈?zhǔn)秸{(diào)用 fold: f => f(x), // 將元素從容器中取出 inspect: () => `Box(${x})`, // 看容器里有啥 }) const nextCharForNumStr = (str) => Box(str) .map(s => s.trim()) .map(i => parseInt(i)) .map(i => i + 1) .map(i => String.fromCharCode(i)) .fold(c => c.toLowerCase()) // 可以輕易地繼續(xù)調(diào)用新的函數(shù) nextCharForNumStr(" 64 ") // a
此外創(chuàng)建一個(gè)容器,除了像函數(shù)一樣直接傳遞參數(shù)以外,還可以使用靜態(tài)方法 of。
函數(shù)式編程一般約定,函子有一個(gè) of 方法,用來(lái)生成新的容器。
Box(1) === Box.of(1)
其實(shí)這個(gè) Box 就是一個(gè)函子(functor),因?yàn)樗鼘?shí)現(xiàn)了 map 函數(shù)。當(dāng)然你也可以叫它 Mappable 或者其他名稱。
不過(guò)為了保持與范疇學(xué)定義的名稱一致,我們就站在巨人的肩膀上不要再發(fā)明新名詞啦~(后面小節(jié)的各種奇怪名詞也是來(lái)源于數(shù)學(xué)名詞)。
functor 是實(shí)現(xiàn)了 map 函數(shù)并遵守一些特定規(guī)則的容器類(lèi)型。
那么這些特定的規(guī)則具體是什么咧?
1. 規(guī)則一:
fx.map(f).map(g) === fx.map(x => g(f(x)))
這其實(shí)就是函數(shù)組合...
2. 規(guī)則二:
const id = x => x fx.map(id) === id(fx)4.2.Either / Maybe
假設(shè)現(xiàn)在有個(gè)需求:獲取對(duì)應(yīng)顏色的十六進(jìn)制的 RGB 值,并返回去掉#后的大寫(xiě)值。
const findColor = (name) => ({ red: "#ff4444", blue: "#3b5998", yellow: "#fff68f", })[name] const redColor = findColor("red") .slice(1) .toUpperCase() // FF4444 const greenColor = findColor("green") .slice(1) .toUpperCase() // Uncaught TypeError: // Cannot read property "slice" of undefined
以上代碼在輸入已有顏色的 key 值時(shí)運(yùn)行良好,不過(guò)一旦傳入其他顏色就會(huì)報(bào)錯(cuò)。咋辦咧?
暫且不提條件判斷和各種奇技淫巧的錯(cuò)誤處理。咱們來(lái)先看看函數(shù)式的解決方案~
函數(shù)式將錯(cuò)誤處理抽象成一個(gè) Either 容器,而這個(gè)容器由兩個(gè)子容器 Right 和 Left 組成。
// Either 由 Right 和 Left 組成 const Left = (x) => ({ map: f => Left(x), // 忽略傳入的 f 函數(shù) fold: (f, g) => f(x), // 使用左邊的函數(shù) inspect: () => `Left(${x})`, // 看容器里有啥 }) const Right = (x) => ({ map: f => Right(f(x)), // 返回容器為了鏈?zhǔn)秸{(diào)用 fold: (f, g) => g(x), // 使用右邊的函數(shù) inspect: () => `Right(${x})`, // 看容器里有啥 }) // 來(lái)測(cè)試看看~ const right = Right(4) .map(x => x * 7 + 1) .map(x => x / 2) right.inspect() // Right(14.5) right.fold(e => "error", x => x) // 14.5 const left = Left(4) .map(x => x * 7 + 1) .map(x => x / 2) left.inspect() // Left(4) left.fold(e => "error", x => x) // error
可以看出 Right 和 Left 相似于 Box:
最大的不同就是 fold 函數(shù),這里需要傳兩個(gè)回調(diào)函數(shù),左邊的給 Left 使用,右邊的給 Right 使用。
其次就是 Left 的 map 函數(shù)忽略了傳入的函數(shù)(因?yàn)槌鲥e(cuò)了嘛,當(dāng)然不能繼續(xù)執(zhí)行啦)。
現(xiàn)在讓我們回到之前的問(wèn)題來(lái)~
const fromNullable = (x) => x == null ? Left(null) : Right(x) const findColor = (name) => fromNullable(({ red: "#ff4444", blue: "#3b5998", yellow: "#fff68f", })[name]) findColor("green") .map(c => c.slice(1)) .fold( e => "no color", c => c.toUpperCase() ) // no color
從以上代碼不知道各位讀者老爺們有沒(méi)有看出使用 Either 的好處,那就是可以放心地對(duì)于這種類(lèi)型的數(shù)據(jù)進(jìn)行任何操作,而不是在每個(gè)函數(shù)里面小心翼翼地進(jìn)行參數(shù)檢查。
4.3.Chain / FlatMap / bind / >>=假設(shè)現(xiàn)在有個(gè) json 文件里面保存了端口,我們要讀取這個(gè)文件獲取端口,要是出錯(cuò)了返回默認(rèn)值 3000。
// config.json { "port": 8888 } // chain.js const fs = require("fs") const getPort = () => { try { const str = fs.readFileSync("config.json") const { port } = JSON.parse(str) return port } catch(e) { return 3000 } } const result = getPort()
so easy~,下面讓我們來(lái)用 Either 來(lái)重構(gòu)下看看效果。
const fs = require("fs") const Left = (x) => ({ ... }) const Right = (x) => ({ ... }) const tryCatch = (f) => { try { return Right(f()) } catch (e) { return Left(e) } } const getPort = () => tryCatch( () => fs.readFileSync("config.json") ) .map(c => JSON.parse(c)) .fold(e => 3000, c => c.port)
啊,常規(guī)操作,看起來(lái)不錯(cuò)喲~不錯(cuò)你個(gè)蛇頭...!
以上代碼有個(gè) bug,當(dāng) json 文件寫(xiě)的有問(wèn)題時(shí),在 JSON.parse 時(shí)會(huì)出錯(cuò),所以這步也要用 tryCatch 包起來(lái)。
但是,問(wèn)題來(lái)了...
返回值這時(shí)候可能是 Right(Right("")) 或者 Right(Left(e))(想想為什么不是 Left(Right("")) 或者 Left(Left(e)))。
也就是說(shuō)我們現(xiàn)在得到的是兩層容器,就像俄羅斯套娃一樣...
要取出容器中的容器中的值,我們就需要 fold 兩次...?。ㄈ羰窃俣鄮讓?..)
因缺思廳,所以聰明機(jī)智的函數(shù)式又想出一個(gè)新方法 chain~,其實(shí)很簡(jiǎn)單,就是我知道這里要返回容器了,那就不要再用容器包了唄。
... const Left = (x) => ({ ... chain: f => Left(x) // 和 map 一樣,直接返回 Left }) const Right = (x) => ({ ... chain: f => f(x), // 直接返回,不使用容器再包一層了 }) const tryCatch = (f) => { ... } const getPort = () => tryCatch( () => fs.readFileSync("config.json") ) .chain(c => tryCatch(() => JSON.parse(c))) // 使用 chain 和 tryCatch .fold( e => 3000, c => c.port )
其實(shí)這里的 Left 和 Right 就是單子(Monad),因?yàn)樗鼘?shí)現(xiàn)了 chain 函數(shù)。
monad 是實(shí)現(xiàn)了 chain 函數(shù)并遵守一些特定規(guī)則的容器類(lèi)型。
在繼續(xù)介紹這些特定規(guī)則前,我們先定義一個(gè) join 函數(shù):
// 這里的 m 指的是一種 Monad 實(shí)例 const join = m => m.chain(x => x)
規(guī)則一:
join(m.map(join)) === join(join(m))
規(guī)則二:
// 這里的 M 指的是一種 Monad 類(lèi)型 join(M.of(m)) === join(m.map(M.of))
這條規(guī)則說(shuō)明了 map 可被 chain 和 of 所定義。
m.map(f) === m.chain(x => M.of(f(x)))
也就是說(shuō) Monad 一定是 Functor
Monad 十分強(qiáng)大,之后我們將利用它處理各種副作用。但別對(duì)其感到困惑,chain 的主要作用不過(guò)將兩種不同的類(lèi)型連接(join)在一起罷了。
4.4.半群(Semigroup)定義一:對(duì)于非空集合 S,若在 S 上定義了二元運(yùn)算 ○,使得對(duì)于任意的 a, b ∈ S,有 a ○ b ∈ S,則稱 {S, ○} 為廣群。定義二:若 {S, ○} 為廣群,且運(yùn)算 ○ 還滿足結(jié)合律,即:任意 a, b, c ∈ S,有 (a ○ b) ○ c = a ○ (b ○ c),則稱 {S, ○} 為半群。
舉例來(lái)說(shuō),JavaScript 中有 concat 方法的對(duì)象都是半群。
// 字符串和 concat 是半群 "1".concat("2").concat("3") === "1".concat("2".concat("3")) // 數(shù)組和 concat 是半群 [1].concat([2]).concat([3]) === [1].concat([2].concat([3]))
雖然理論上對(duì)于
數(shù)字相加返回的仍然是數(shù)字(廣群)
加法滿足結(jié)合律(半群)
但是數(shù)字并沒(méi)有 concat 方法
沒(méi)事兒,讓我們來(lái)實(shí)現(xiàn)這個(gè)由
const Sum = (x) => ({ x, concat: ({ x: y }) => Sum(x + y), // 采用解構(gòu)獲取值 inspect: () => `Sum(${x})`, }) Sum(1) .concat(Sum(2)) .inspect() // Sum(3)
除此之外,
const All = (x) => ({ x, concat: ({ x: y }) => All(x && y), // 采用解構(gòu)獲取值 inspect: () => `All(${x})`, }) All(true) .concat(All(false)) .inspect() // All(false)
最后,讓我們對(duì)于字符串創(chuàng)建一個(gè)新的半群 First,顧名思義,它會(huì)忽略除了第一個(gè)參數(shù)以外的內(nèi)容。
const First = (x) => ({ x, concat: () => First(x), // 忽略后續(xù)的值 inspect: () => `First(${x})`, }) First("blah") .concat(First("yoyoyo")) .inspect() // First("blah")
咿呀喲?是不是感覺(jué)這個(gè)半群和其他半群好像有點(diǎn)兒不太一樣,不過(guò)具體是啥又說(shuō)不上來(lái)...?
這個(gè)問(wèn)題留給下個(gè)小節(jié)。在此先說(shuō)下這玩意兒有啥用。
const data1 = { name: "steve", isPaid: true, points: 10, friends: ["jame"], } const data2 = { name: "steve", isPaid: false, points: 2, friends: ["young"], }
假設(shè)有兩個(gè)數(shù)據(jù),需要將其合并,那么利用半群,我們可以對(duì) name 應(yīng)用 First,對(duì)于 isPaid 應(yīng)用 All,對(duì)于 points 應(yīng)用 Sum,最后的 friends 已經(jīng)是半群了...
const Sum = (x) => ({ ... }) const All = (x) => ({ ... }) const First = (x) => ({ ... }) const data1 = { name: First("steve"), isPaid: All(true), points: Sum(10), friends: ["jame"], } const data2 = { name: First("steve"), isPaid: All(false), points: Sum(2), friends: ["young"], } const concatObj = (obj1, obj2) => Object.entries(obj1) .map(([ key, val ]) => ({ // concat 兩個(gè)對(duì)象的值 [key]: val.concat(obj2[key]), })) .reduce((acc, cur) => ({ ...acc, ...cur })) concatObj(data1, data2) /* { name: First("steve"), isPaid: All(false), points: Sum(12), friends: ["jame", "young"], } */4.5.幺半群(Monoid)
幺半群是一個(gè)存在單位元(幺元)的半群。
半群我們都懂,不過(guò)啥是單位元?
單位元:對(duì)于半群,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
舉例來(lái)說(shuō),對(duì)于數(shù)字加法這個(gè)半群來(lái)說(shuō),0就是它的單位元,所以
對(duì)于
對(duì)于
對(duì)于
對(duì)于
對(duì)于
那么
顯然我們并不能找到這樣一個(gè)單位元 e 滿足
First(e).concat(First("steve")) === First("steve").concat(First(e))
這就是上一節(jié)留的小懸念,為何會(huì)感覺(jué) First 與 Sum 和 All 不太一樣的原因。
格嘰格嘰,這兩者有啥具體的差別么?
其實(shí)看到幺半群的第一反應(yīng)應(yīng)該是默認(rèn)值或初始值,例如 reduce 函數(shù)的第二個(gè)參數(shù)就是傳入一個(gè)初始值或者說(shuō)是默認(rèn)值。
// sum const Sum = (x) => ({ ... }) Sum.empty = () => Sum(0) // 單位元 const sum = xs => xs.reduce((acc, cur) => acc + cur, 0) sum([1, 2, 3]) // 6 sum([]) // 0,而不是報(bào)錯(cuò)! // all const All = (x) => ({ ... }) All.empty = () => All(true) // 單位元 const all = xs => xs.reduce((acc, cur) => acc && cur, true) all([true, false, true]) // false all([]) // true,而不是報(bào)錯(cuò)! // first const First = (x) => ({ ... }) const first = xs => xs.reduce(acc, cur) => acc) first(["steve", "jame", "young"]) // steve first([]) // boom!!!
從以上代碼可以看出幺半群比半群要安全得多,
4.6.foldMap 1.套路在上一節(jié)中幺半群的使用代碼中,如果傳入的都是幺半群實(shí)例而不是原始類(lèi)型的話,你會(huì)發(fā)現(xiàn)其實(shí)都是一個(gè)套路...
const Monoid = (x) => ({ ... }) const monoid = xs => xs.reduce( (acc, cur) => acc.concat(cur), // 使用 concat 結(jié)合 Monoid.empty() // 傳入幺元 ) monoid([Monoid(a), Monoid(b), Monoid(c)]) // 傳入幺半群實(shí)例
所以對(duì)于思維高度抽象的函數(shù)式來(lái)說(shuō),這樣的代碼肯定是需要繼續(xù)重構(gòu)精簡(jiǎn)的~
2.List、Map在講解如何重構(gòu)之前,先介紹兩個(gè)炒雞常用的不可變數(shù)據(jù)結(jié)構(gòu):List、Map。
顧名思義,正好對(duì)應(yīng)原生的 Array 和 Object。
3.利用 List、Map 重構(gòu)因?yàn)?immutable 庫(kù)中的 List 和 Map 并沒(méi)有 empty 屬性和 fold 方法,所以我們首先擴(kuò)展 List 和 Map~
import { List, Map } from "immutable" const derived = { fold (empty) { return this.reduce((acc, cur) => acc.concat(cur), empty) }, } List.prototype.empty = List() List.prototype.fold = derived.fold Map.prototype.empty = Map({}) Map.prototype.fold = derived.fold // from https://github.com/DrBoolean/immutable-ext
這樣一來(lái)上一節(jié)的代碼就可以精簡(jiǎn)成這樣:
List.of(1, 2, 3) .map(Sum) .fold(Sum.empty()) // Sum(6) List().fold(Sum.empty()) // Sum(0) Map({ steve: 1, young: 3 }) .map(Sum) .fold(Sum.empty()) // Sum(4) Map().fold(Sum.empty()) // Sum(0)4.利用 foldMap 重構(gòu)
注意到 map 和 fold 這兩步操作,從邏輯上來(lái)說(shuō)是一個(gè)操作,所以我們可以新增 foldMap 方法來(lái)結(jié)合兩者。
import { List, Map } from "immutable" const derived = { fold (empty) { return this.foldMap(x => x, empty) }, foldMap (f, empty) { return empty != null // 幺半群中將 f 的調(diào)用放在 reduce 中,提高效率 ? this.reduce( (acc, cur, idx) => acc.concat(f(cur, idx)), empty ) : this // 在 map 中調(diào)用 f 是因?yàn)榭紤]到空的情況 .map(f) .reduce((acc, cur) => acc.concat(cur)) }, } List.prototype.empty = List() List.prototype.fold = derived.fold List.prototype.foldMap = derived.foldMap Map.prototype.empty = Map({}) Map.prototype.fold = derived.fold Map.prototype.foldMap = derived.foldMap // from https://github.com/DrBoolean/immutable-ext
所以最終版長(zhǎng)這樣:
List.of(1, 2, 3) .foldMap(Sum, Sum.empty()) // Sum(6) List() .foldMap(Sum, Sum.empty()) // Sum(0) Map({ a: 1, b: 3 }) .foldMap(Sum, Sum.empty()) // Sum(4) Map() .foldMap(Sum, Sum.empty()) // Sum(0)4.7.LazyBox
下面我們要來(lái)實(shí)現(xiàn)一個(gè)新容器 LazyBox。
顧名思義,這個(gè)容器很懶...
雖然你可以不停地用 map 給它分配任務(wù),但是只要你不調(diào)用 fold 方法催它執(zhí)行(就像 deadline 一樣),它就死活不執(zhí)行...
const LazyBox = (g) => ({ map: f => LazyBox(() => f(g())), fold: f => f(g()), }) const result = LazyBox(() => " 64 ") .map(s => s.trim()) .map(i => parseInt(i)) .map(i => i + 1) .map(i => String.fromCharCode(i)) // 沒(méi)有 fold 死活不執(zhí)行 result.fold(c => c.toLowerCase()) // a4.8.Task 1.基本介紹
有了上一節(jié)中 LazyBox 的基礎(chǔ)之后,接下來(lái)我們來(lái)創(chuàng)建一個(gè)新的類(lèi)型 Task。
首先 Task 的構(gòu)造函數(shù)可以接收一個(gè)函數(shù)以便延遲計(jì)算,當(dāng)然也可以用 of 方法來(lái)創(chuàng)建實(shí)例,很自然的也有 map、chain、concat、empty 等方法。
與眾不同的是它有個(gè) fork 方法(類(lèi)似于 LazyBox 中的 fold 方法,在 fork 執(zhí)行前其他函數(shù)并不會(huì)執(zhí)行),以及一個(gè) rejected 方法,類(lèi)似于 Left,忽略后續(xù)的操作。
import Task from "data.task" const showErr = e => console.log(`err: ${e}`) const showSuc = x => console.log(`suc: ${x}`) Task .of(1) .fork(showErr, showSuc) // suc: 1 Task .of(1) .map(x => x + 1) .fork(showErr, showSuc) // suc: 2 // 類(lèi)似 Left Task .rejected(1) .map(x => x + 1) .fork(showErr, showSuc) // err: 1 Task .of(1) .chain(x => new Task.of(x + 1)) .fork(showErr, showSuc) // suc: 22.使用示例
接下來(lái)讓我們做一個(gè)發(fā)射飛彈的程序~
const lauchMissiles = () => ( // 和 promise 很像,不過(guò) promise 會(huì)立即執(zhí)行 // 而且參數(shù)的位置也相反 new Task((rej, res) => { console.log("lauchMissiles") res("missile") }) ) // 繼續(xù)對(duì)之前的任務(wù)添加后續(xù)操作(duang~給飛彈加特技?。?const app = lauchMissiles() .map(x => x + "!") // 這時(shí)才執(zhí)行(發(fā)射飛彈) app.fork(showErr, showSuc)3.原理意義
上面的代碼乍一看好像沒(méi)啥用,只不過(guò)是把待執(zhí)行的代碼用函數(shù)包起來(lái)了嘛,這還能吹上天?
還記得前面章節(jié)說(shuō)到的副作用么?雖然說(shuō)使用純函數(shù)是沒(méi)有副作用的,但是日常項(xiàng)目中有各種必須處理的副作用。
所以我們將有副作用的代碼給包起來(lái)之后,這些新函數(shù)就都變成了純函數(shù),這樣我們的整個(gè)應(yīng)用的代碼都是純的~,并且在代碼真正執(zhí)行前(fork 前)還可以不斷地 compose 別的函數(shù),為我們的應(yīng)用不斷添加各種功能,這樣一來(lái)整個(gè)應(yīng)用的代碼流程都會(huì)十分的簡(jiǎn)潔漂亮。
4.異步嵌套示例以下代碼做了 3 件事:
讀取 config1.json 中的數(shù)據(jù)
將內(nèi)容中的 8 替換成 6
將新內(nèi)容寫(xiě)到 config2.json 中
import fs from "fs" const app = () => ( fs.readFile("config1.json", "utf-8", (err, contents) => { if (err) throw err const newContents = content.replace(/8/g, "6") fs.writeFile("config2.json", newContents, (err, _) => { if (err) throw err console.log("success!") }) }) )
讓我們用 Task 來(lái)改寫(xiě)一下~
import fs from "fs" import Task from "data.task" const cfg1 = "config1.json" const cfg2 = "config2.json" const readFile = (file, enc) => ( new Task((rej, res) => fs.readFile(file, enc, (err, str) => err ? rej(err) : res(str) ) ) ) const writeFile = (file, str) => ( new Task((rej, res) => fs.writeFile(file, str, (err, suc) => err ? rej(err) : res(suc) ) ) ) const app = readFile(cfg1, "utf-8") .map(str => str.replace(/8/g, "6")) .chain(str => writeFile(cfg2, str)) app.fork( e => console.log(`err: ${e}`), x => console.log(`suc: ${x}`) )
代碼一目了然,按照線性的先后順序完成了任務(wù),并且在其中還可以隨意地插入或修改需求~
4.9.Applicative Functor 1.問(wèn)題引入Applicative Functor 提供了讓不同的函子(functor)互相應(yīng)用的能力。
為啥我們需要函子的互相應(yīng)用?什么是互相應(yīng)用?
先來(lái)看個(gè)簡(jiǎn)單例子:
const add = x => y => x + y add(Box.of(2))(Box.of(3)) // NaN Box(2).map(add).inspect() // Box(y => 2 + y)
現(xiàn)在我們有了一個(gè)容器,它的內(nèi)部值為局部調(diào)用(partially applied)后的函數(shù)。接著我們想讓它應(yīng)用到 Box(3) 上,最后得到 Box(5) 的預(yù)期結(jié)果。
說(shuō)到從容器中取值,那肯定第一個(gè)想到 chain 方法,讓我們來(lái)試一下:
Box(2) .chain(x => Box(3).map(add(x))) .inspect() // Box(5)
成功實(shí)現(xiàn)~,BUT,這種實(shí)現(xiàn)方法有個(gè)問(wèn)題,那就是單子(Monad)的執(zhí)行順序問(wèn)題。
我們這樣實(shí)現(xiàn)的話,就必須等 Box(2) 執(zhí)行完畢后,才能對(duì) Box(3) 進(jìn)行求值。假如這是兩個(gè)異步任務(wù),那么完全無(wú)法并行執(zhí)行。
別慌,吃口藥~2.基本介紹
下面介紹下主角:ap~:
const Box = (x) => ({ // 這里 box 是另一個(gè) Box 的實(shí)例,x 是函數(shù) ap: box => box.map(x), ... }) Box(add) // Box(y => 2 + y) ,咦?在哪兒見(jiàn)過(guò)? .ap(Box(2)) .ap(Box(3)) // Box(5)
運(yùn)算規(guī)則
F(x).map(f) === F(f).ap(F(x)) // 這就是為什么 Box(2).map(add) === Box(add).ap(Box(2))3.Lift 家族
由于日常編寫(xiě)代碼的時(shí)候直接用 ap 的話模板代碼太多,所以一般通過(guò)使用 Lift 家族系列函數(shù)來(lái)簡(jiǎn)化。
// F 該從哪兒來(lái)? const fakeLiftA2 = f => fx => fy => F(f).ap(fx).ap(fy) // 應(yīng)用運(yùn)算規(guī)則轉(zhuǎn)換一下~ const liftA2 = f => fx => fy => fx.map(f).ap(fy) liftA2(add, Box(2), Box(4)) // Box(6) // 同理 const liftA3 = f => fx => fy => fz => fx.map(f).ap(fy).ap(fz) const liftA4 = ... ... const liftAN = ...4.Lift 應(yīng)用
例1
// 假裝是個(gè) jQuery 接口~ const $ = selector => Either.of({ selector, height: 10 }) const getScreenSize = screen => head => foot => screen - (head.height + foot.height) liftA2(getScreenSize(800))($("header"))($("footer")) // Right(780)
例2
// List 的笛卡爾乘積 List.of(x => y => z => [x, y, z].join("-")) .ap(List.of("tshirt", "sweater")) .ap(List.of("white", "black")) .ap(List.of("small", "medium", "large"))
例3
const Db = ({ find: (id, cb) => new Task((rej, res) => setTimeout(() => res({ id, title: `${id}`}), 100) ) }) const reportHeader = (p1, p2) => `Report: ${p1.title} compared to ${p2.title}` Task.of(p1 => p2 => reportHeader(p1, p2)) .ap(Db.find(20)) .ap(Db.find(8)) .fork(console.error, console.log) // Report: 20 compared to 8 liftA2 (p1 => p2 => reportHeader(p1, p2)) (Db.find(20)) (Db.find(8)) .fork(console.error, console.log) // Report: 20 compared to 84.10.Traversable 1.問(wèn)題引入
import fs from "fs" // 詳見(jiàn) 4.8. const readFile = (file, enc) => ( new Task((rej, res) => ...) ) const files = ["a.js", "b.js"] // [Task, Task],我們得到了一個(gè) Task 的數(shù)組 files.map(file => readFile(file, "utf-8"))
然而我們想得到的是一個(gè)包含數(shù)組的 Task([file1, file2]),這樣就可以調(diào)用它的 fork 方法,查看執(zhí)行結(jié)果。
為了解決這個(gè)問(wèn)題,函數(shù)式編程一般用一個(gè)叫做 traverse 的方法來(lái)實(shí)現(xiàn)。
files .traverse(Task.of, file => readFile(file, "utf-8")) .fork(console.error, console.log)
traverse 方法第一個(gè)參數(shù)是創(chuàng)建函子的函數(shù),第二個(gè)參數(shù)是要應(yīng)用在函子上的函數(shù)。
2.實(shí)現(xiàn)其實(shí)以上代碼有 bug...,因?yàn)閿?shù)組 Array 是沒(méi)有 traverse 方法的。沒(méi)事兒,讓我們來(lái)實(shí)現(xiàn)一下~
Array.prototype.empty = [] // traversable Array.prototype.traverse = function (point, fn) { return this.reduce( (acc, cur) => acc .map(z => y => z.concat(y)) .ap(fn(cur)), point(this.empty) ) }
看著有點(diǎn)兒暈?
不急,首先看代碼主體是一個(gè) reduce,這個(gè)很熟了,就是從左到右遍歷元素,其中的第二個(gè)參數(shù)傳遞的就是幺半群(monoid)的單位元(empty)。
再看第一個(gè)參數(shù),主要就是通過(guò) applicative functor 調(diào)用 ap 方法,再將其執(zhí)行結(jié)果使用 concat 方法合并到數(shù)組中。
所以最后返回的就是 Task([foo, bar]),因此我們可以調(diào)用 fork 方法執(zhí)行它。
4.11.自然變換(Natural Transformations) 1.基本概念自然變換就是一個(gè)函數(shù),接受一個(gè)函子(functor),返回另一個(gè)函子。看看代碼熟悉下~
const boxToEither = b => b.fold(Right)
這個(gè) boxToEither 函數(shù)就是一個(gè)自然變換(nt),它將函子 Box 轉(zhuǎn)換成了另一個(gè)函子 Either。
那么我們用 Left 行不行呢?
答案是不行!
因?yàn)樽匀蛔儞Q不僅是將一個(gè)函子轉(zhuǎn)換成另一個(gè)函子,它還滿足以下規(guī)則:
nt(x).map(f) == nt(x.map(f))
舉例來(lái)說(shuō)就是:
const res1 = boxToEither(Box(100)) .map(x => x * 2) const res2 = boxToEither( Box(100).map(x => x * 2) ) res1 === res2 // Right(200)
即先對(duì)函子 a 做改變?cè)賹⑵滢D(zhuǎn)換為函子 b,是等價(jià)于先將函子 a 轉(zhuǎn)換為函子 b 再做改變。
顯然,Left 并不滿足這個(gè)規(guī)則。所以任何滿足這個(gè)規(guī)則的函數(shù)都是自然變換。
2.應(yīng)用場(chǎng)景1.例1:得到一個(gè)數(shù)組小于等于 100 的最后一個(gè)數(shù)的兩倍的值
const arr = [2, 400, 5, 1000] const first = xs => fromNullable(xs[0]) const double = x => x * 2 const getLargeNums = xs => xs.filter(x => x > 100) first( getLargeNums(arr).map(double) )
根據(jù)自然變換,它顯然和 first(getLargeNums(arr)).map(double) 是等價(jià)的。但是后者顯然性能好得多。
再來(lái)看一個(gè)更復(fù)雜一點(diǎn)兒的例子:
2.例2:找到 id 為 3 的用戶的最好的朋友的 id
// 假 api const fakeApi = (id) => ({ id, name: "user1", bestFriendId: id + 1, }) // 假 Db const Db = { find: (id) => new Task( (rej, res) => ( res(id > 2 ? Right(fakeApi(id)) : Left("not found") ) ) ) }
// Task(Either(user)) const zero = Db.find(3) // 第一版 // Task(Either(Task(Either(user)))) ??? const one = zero .map(either => either .map(user => Db .find(user.bestFriendId) ) ) .fork( console.error, either => either // Either(Task(Either(user))) .map(t => t.fork( // Task(Either(user)) console.error, either => either .map(console.log), // Either(user) )) )
這是什么鬼???
肯定不能這么干...
// Task(Either(user)) const zero = Db.find(3) // 第二版 const two = zero .chain(either => either .fold(Task.rejected, Task.of) // Task(user) .chain(user => Db .find(user.bestFriendId) // Task(Either(user)) ) .chain(either => either .fold(Task.rejected, Task.of) // Task(user) ) ) .fork( console.error, console.log, )
第二版的問(wèn)題是多余的嵌套代碼。
// Task(Either(user)) const zero = Db.find(3) // 第三版 const three = zero .chain(either => either .fold(Task.rejected, Task.of) // Task(user) ) .chain(user => Db .find(user.bestFriendId) // Task(Either(user)) ) .chain(either => either .fold(Task.rejected, Task.of) // Task(user) ) .fork( console.error, console.log, )
第三版的問(wèn)題是多余的重復(fù)邏輯。
// Task(Either(user)) const zero = Db.find(3) // 這其實(shí)就是自然變換 // 將 Either 變換成 Task const eitherToTask = (e) => ( e.fold(Task.rejected, Task.of) ) // 第四版 const four = zero .chain(eitherToTask) // Task(user) .chain(user => Db .find(user.bestFriendId) // Task(Either(user)) ) .chain(eitherToTask) // Task(user) .fork( console.error, console.log, ) // 出錯(cuò)版 const error = Db.find(2) // Task(Either(user)) // Task.rejected("not found") .chain(eitherToTask) // 這里永遠(yuǎn)不會(huì)被調(diào)用,被跳過(guò)了 .chain(() => console.log("hey man")) ... .fork( console.error, // not found console.log, )4.12.同構(gòu)(Isomorphism)
同構(gòu)是在數(shù)學(xué)對(duì)象之間定義的一類(lèi)映射,它能揭示出在這些對(duì)象的屬性或者操作之間存在的關(guān)系。
簡(jiǎn)單來(lái)說(shuō)就是兩種不同類(lèi)型的對(duì)象經(jīng)過(guò)變形,保持結(jié)構(gòu)并且不丟失數(shù)據(jù)。
具體怎么做到的呢?
其實(shí)同構(gòu)就是一對(duì)兒函數(shù):to 和 from,遵守以下規(guī)則:
to(from(x)) === x from(to(y)) === y
這其實(shí)說(shuō)明了這兩個(gè)類(lèi)型都能夠無(wú)損地保存同樣的信息。
1. 例如 String 和 [Char] 就是同構(gòu)的。// String ~ [Char] const Iso = (to, from) => ({ to, from }) const chars = Iso( s => s.split(""), c => c.join("") ) const str = "hello world" chars.from(chars.to(str)) === str
這能有啥用呢?
const truncate = (str) => ( chars.from( // 我們先用 to 方法將其轉(zhuǎn)成數(shù)組 // 這樣就能使用數(shù)組的各類(lèi)方法 chars.to(str).slice(0, 3) ).concat("...") ) truncate(str) // hel...2. 再來(lái)看看最多有一個(gè)參數(shù)的數(shù)組 [a] 和 Either 的同構(gòu)關(guān)系
// [a] ~ Either null a const singleton = Iso( e => e.fold(() => [], x => [x]), ([ x ]) => x ? Right(x) : Left() ) const filterEither = (e, pred) => singleton .from( singleton .to(e) .filter(pred) ) const getUCH = (str) => filterEither( Right(str), x => x.match(/h/ig) ).map(x => x.toUpperCase()) getUCH("hello") // Right(HELLO) getUCH("ello") // Left(undefined)參考資料
JS函數(shù)式編程指南
Pointfree 編程風(fēng)格指南
Hey Underscore, You"re Doing It Wrong!
Functional Concepts with JavaScript: Part I
Professor Frisby Introduces Composable Functional JavaScript
函數(shù)式編程入門(mén)教程
What are Functional Programming, Monad, Monoid, Applicative, Functor ??
相關(guān)文章JavaScript 函數(shù)式編程(一)
JavaScript 函數(shù)式編程(二)
JavaScript 函數(shù)式編程(三)-- 本文
JavaScript 函數(shù)式編程(四)正在醞釀...
以上 to be continued...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/96872.html
摘要:它大致概述并討論了前端工程的實(shí)踐如何學(xué)習(xí)它,以及在年實(shí)踐時(shí)使用什么工具。目的是每年發(fā)布一次內(nèi)容更新。前端實(shí)踐第一部分廣泛描述了前端工程的實(shí)踐。對(duì)大多數(shù)人來(lái)說(shuō),函數(shù)式編程看起來(lái)更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來(lái)了解前端開(kāi)發(fā)實(shí)踐的指南。它大致概述并...
摘要:它大致概述并討論了前端工程的實(shí)踐如何學(xué)習(xí)它,以及在年實(shí)踐時(shí)使用什么工具。目的是每年發(fā)布一次內(nèi)容更新。前端實(shí)踐第一部分廣泛描述了前端工程的實(shí)踐。對(duì)大多數(shù)人來(lái)說(shuō),函數(shù)式編程看起來(lái)更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來(lái)了解前端開(kāi)發(fā)實(shí)踐的指南。它大致概述并...
摘要:它大致概述并討論了前端工程的實(shí)踐如何學(xué)習(xí)它,以及在年實(shí)踐時(shí)使用什么工具。目的是每年發(fā)布一次內(nèi)容更新。前端實(shí)踐第一部分廣泛描述了前端工程的實(shí)踐。對(duì)大多數(shù)人來(lái)說(shuō),函數(shù)式編程看起來(lái)更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來(lái)了解前端開(kāi)發(fā)實(shí)踐的指南。它大致概述并...
摘要:子類(lèi)不是父類(lèi)實(shí)例的問(wèn)題是由類(lèi)式繼承引起的。所以寄生式繼承和構(gòu)造函數(shù)繼承的組合又稱為一種新的繼承方式。但是這里的寄生式繼承處理的不是對(duì)象,而是類(lèi)的原型??瓷先ヂ晕?fù)雜,還得好好研究。 寄生組合式繼承(終極繼承者) 前面學(xué)習(xí)了類(lèi)式繼承和構(gòu)造函數(shù)繼承組合使用,也就是組合繼承,但是這種繼承方式有個(gè)問(wèn)題,就是子類(lèi)不是父類(lèi)的實(shí)例,而子類(lèi)的原型是父類(lèi)的實(shí)例。子類(lèi)不是父類(lèi)實(shí)例的問(wèn)題是由類(lèi)式繼承引起的。...
摘要:函數(shù)式編程,一看這個(gè)詞,簡(jiǎn)直就是學(xué)院派的典范。所以這期周刊,我們就重點(diǎn)引入的函數(shù)式編程,淺入淺出,一窺函數(shù)式編程的思想,可能讓你對(duì)編程語(yǔ)言的理解更加融會(huì)貫通一些。但從根本上來(lái)說(shuō),函數(shù)式編程就是關(guān)于如使用通用的可復(fù)用函數(shù)進(jìn)行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數(shù)式編程(Functional Programming),一...
摘要:在函數(shù)內(nèi)保存數(shù)據(jù)在命令式語(yǔ)言中,函數(shù)內(nèi)部的私有變量局部變量是不能被保存的。從程序的執(zhí)行方式上來(lái)講,局部變量在棧上分配,在函數(shù)執(zhí)行結(jié)束后,所占用的棧被釋放。這一點(diǎn)其實(shí)是破壞它的函數(shù)式特性的。 本文內(nèi)容是我閱讀《JavaScript語(yǔ)言精髓與編程實(shí)踐》時(shí),做的讀書(shū)筆記,周愛(ài)民老師的書(shū)寫(xiě)的太深刻了! 函數(shù)式語(yǔ)言中的函數(shù) 首先要有一個(gè)概念:并不是一個(gè)語(yǔ)言支持函數(shù),這個(gè)語(yǔ)言就可以叫做函數(shù)式語(yǔ)言。...
閱讀 1945·2021-09-22 15:55
閱讀 3583·2021-09-07 10:26
閱讀 702·2019-08-30 15:54
閱讀 740·2019-08-29 16:34
閱讀 899·2019-08-26 14:04
閱讀 3337·2019-08-26 11:47
閱讀 2184·2019-08-26 11:33
閱讀 2346·2019-08-23 15:17