slide 地址javascript
如下內容主要參考自 Professor Frisby Introduces Composable Functional JavaScript
假設有個函數,能夠接收一個來自用戶輸入的數字字符串。咱們須要對其預處理一下,去除多餘空格,將其轉換爲數字並加一,最後返回該值對應的字母。代碼大概長這樣...html
const nextCharForNumStr = (str) => String.fromCharCode(parseInt(str.trim()) + 1) nextCharForNumStr(' 64 ') // "A"
因缺思廳,這代碼嵌套的也太緊湊了,看多了「老闊疼」,趕忙重構一把...java
const nextCharForNumStr = (str) => { const trimmed = str.trim() const number = parseInt(trimmed) const nextNumber = number + 1 return String.fromCharCode(nextNumber) } nextCharForNumStr(' 64 ') // 'A'
很顯然,通過以前內容的薰(xi)陶(nao),一眼就能夠看出這個修訂版代碼很不 Pointfree...git
爲了這些只用一次的中間變量還要去想或者去查翻譯,也是容易「老闊疼」,再改再改~github
const nextCharForNumStr = (str) => [str] .map(s => s.trim()) .map(s => parseInt(s)) .map(i => i + 1) .map(i => String.fromCharCode(i)) nextCharForNumStr(' 64 ') // ['A']
此次藉助數組的 map 方法,咱們將必須的4個步驟拆分紅了4個小函數。編程
這樣一來不再用去想中間變量的名稱到底叫什麼,並且每一步作的事情十分的清晰,一眼就能夠看出這段代碼在幹嗎。json
咱們將本來的字符串變量 str 放在數組中變成了 [str],這裏就像放在一個容器裏同樣。segmentfault
代碼是否是感受好 door~~ 了?
不過在這裏咱們能夠更進一步,讓咱們來建立一個新的類型 Box。咱們將一樣定義 map 方法,讓其實現一樣的功能。api
const Box = (x) => ({ map: f => Box(f(x)), // 返回容器爲了鏈式調用 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()) // 能夠輕易地繼續調用新的函數 nextCharForNumStr(' 64 ') // a
此外建立一個容器,除了像函數同樣直接傳遞參數之外,還可使用靜態方法 of
。數組
函數式編程通常約定,函子有一個 of 方法,用來生成新的容器。
Box(1) === Box.of(1)
其實這個 Box
就是一個函子(functor),由於它實現了 map
函數。固然你也能夠叫它 Mappable
或者其餘名稱。
不過爲了保持與範疇學定義的名稱一致,咱們就站在巨人的肩膀上不要再發明新名詞啦~(後面小節的各類奇怪名詞也是來源於數學名詞)。
functor 是實現了 map 函數並遵照一些特定規則的容器類型。
那麼這些特定的規則具體是什麼咧?
1. 規則一:
fx.map(f).map(g) === fx.map(x => g(f(x)))
這其實就是函數組合...
2. 規則二:
const id = x => x fx.map(id) === id(fx)
假設如今有個需求:獲取對應顏色的十六進制的 RGB 值,並返回去掉#
後的大寫值。
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
值時運行良好,不過一旦傳入其餘顏色就會報錯。咋辦咧?
暫且不提條件判斷和各類奇技淫巧的錯誤處理。我們來先看看函數式的解決方案~
函數式將錯誤處理抽象成一個 Either
容器,而這個容器由兩個子容器 Right
和 Left
組成。
// Either 由 Right 和 Left 組成 const Left = (x) => ({ map: f => Left(x), // 忽略傳入的 f 函數 fold: (f, g) => f(x), // 使用左邊的函數 inspect: () => `Left(${x})`, // 看容器裏有啥 }) const Right = (x) => ({ map: f => Right(f(x)), // 返回容器爲了鏈式調用 fold: (f, g) => g(x), // 使用右邊的函數 inspect: () => `Right(${x})`, // 看容器裏有啥 }) // 來測試看看~ 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
函數,這裏須要傳兩個回調函數,左邊的給 Left
使用,右邊的給 Right
使用。Left
的 map
函數忽略了傳入的函數(由於出錯了嘛,固然不能繼續執行啦)。如今讓咱們回到以前的問題來~
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
從以上代碼不知道各位讀者老爺們有沒有看出使用 Either
的好處,那就是能夠放心地對於這種類型的數據進行任何操做,而不是在每一個函數裏面當心翼翼地進行參數檢查。
Chain
/ FlatMap
/ bind
/ >>=
假設如今有個 json 文件裏面保存了端口,咱們要讀取這個文件獲取端口,要是出錯了返回默認值 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~,下面讓咱們來用 Either 來重構下看看效果。
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)
啊,常規操做,看起來不錯喲~不錯你個蛇頭...!
以上代碼有個 bug
,當 json
文件寫的有問題時,在 JSON.parse
時會出錯,因此這步也要用 tryCatch
包起來。
可是,問題來了...
返回值這時候多是 Right(Right(''))
或者 Right(Left(e))
(想一想爲何不是 Left(Right(''))
或者 Left(Left(e)))
。
也就是說咱們如今獲得的是兩層容器,就像俄羅斯套娃同樣...
要取出容器中的容器中的值,咱們就須要 fold
兩次...!(如果再多幾層...)
因缺思廳,因此聰明機智的函數式又想出一個新方法 chain~,其實很簡單,就是我知道這裏要返回容器了,那就不要再用容器包了唄。
... 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 )
其實這裏的 Left
和 Right
就是單子(Monad),由於它實現了 chain
函數。
monad 是實現了 chain 函數並遵照一些特定規則的容器類型。
在繼續介紹這些特定規則前,咱們先定義一個 join
函數:
// 這裏的 m 指的是一種 Monad 實例 const join = m => m.chain(x => x)
join(m.map(join)) === join(join(m))
// 這裏的 M 指的是一種 Monad 類型 join(M.of(m)) === join(m.map(M.of))
這條規則說明了 map
可被 chain
和 of
所定義。
m.map(f) === m.chain(x => M.of(f(x)))
也就是說 Monad
必定是 Functor
Monad
十分強大,以後咱們將利用它處理各類反作用。但別對其感到困惑,chain
的主要做用不過將兩種不一樣的類型鏈接(join
)在一塊兒罷了。
定義一:對於非空集合 S,若在 S 上定義了二元運算 ○,使得對於任意的 a, b ∈ S,有 a ○ b ∈ S,則稱 {S, ○} 爲廣羣。定義二:若 {S, ○} 爲廣羣,且運算 ○ 還知足結合律,即:任意 a, b, c ∈ S,有 (a ○ b) ○ c = a ○ (b ○ c),則稱 {S, ○} 爲半羣。
舉例來講,JavaScript 中有 concat 方法的對象都是半羣。
// 字符串和 concat 是半羣 '1'.concat('2').concat('3') === '1'.concat('2'.concat('3')) // 數組和 concat 是半羣 [1].concat([2]).concat([3]) === [1].concat([2].concat([3]))
雖然理論上對於 <Number, +>
來講它符合半羣的定義:
可是數字並無 concat 方法
沒事兒,讓咱們來實現這個由 <Number, +>
組成的半羣 Sum。
const Sum = (x) => ({ x, concat: ({ x: y }) => Sum(x + y), // 採用解構獲取值 inspect: () => `Sum(${x})`, }) Sum(1) .concat(Sum(2)) .inspect() // Sum(3)
除此以外,<Boolean, &&>
也知足半羣的定義~
const All = (x) => ({ x, concat: ({ x: y }) => All(x && y), // 採用解構獲取值 inspect: () => `All(${x})`, }) All(true) .concat(All(false)) .inspect() // All(false)
最後,讓咱們對於字符串建立一個新的半羣 First,顧名思義,它會忽略除了第一個參數之外的內容。
const First = (x) => ({ x, concat: () => First(x), // 忽略後續的值 inspect: () => `First(${x})`, }) First('blah') .concat(First('yoyoyo')) .inspect() // First('blah')
咿呀喲?是否是感受這個半羣和其餘半羣好像有點兒不太同樣,不過具體是啥又說不上來...?
這個問題留給下個小節。在此先說下這玩意兒有啥用。
const data1 = { name: 'steve', isPaid: true, points: 10, friends: ['jame'], } const data2 = { name: 'steve', isPaid: false, points: 2, friends: ['young'], }
假設有兩個數據,須要將其合併,那麼利用半羣,咱們能夠對 name 應用 First,對於 isPaid 應用 All,對於 points 應用 Sum,最後的 friends 已是半羣了...
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 兩個對象的值 [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'], } */
幺半羣是一個存在單位元(幺元)的半羣。
半羣咱們都懂,不過啥是單位元?
單位元:對於半羣 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
舉例來講,對於數字加法這個半羣來講,0就是它的單位元,因此 <Number, +, 0>
就構成一個幺半羣。同理:
<Number, *>
來講單位元就是 1<Boolean, &&>
來講單位元就是 true<Boolean, ||>
來講單位元就是 false<Number, Min>
來講單位元就是 Infinity<Number, Max>
來講單位元就是 -Infinity那麼 <String, First>
是幺半羣麼?
顯然咱們並不能找到這樣一個單位元 e 知足
First(e).concat(First('steve')) === First('steve').concat(First(e))
這就是上一節留的小懸念,爲什麼會感受 First 與 Sum 和 All 不太同樣的緣由。
格嘰格嘰,這二者有啥具體的差異麼?
其實看到幺半羣的第一反應應該是默認值或初始值,例如 reduce 函數的第二個參數就是傳入一個初始值或者說是默認值。
// 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,而不是報錯! // 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,而不是報錯! // first const First = (x) => ({ ... }) const first = xs => xs.reduce(acc, cur) => acc) first(['steve', 'jame', 'young']) // steve first([]) // boom!!!
從以上代碼能夠看出幺半羣比半羣要安全得多,
在上一節中幺半羣的使用代碼中,若是傳入的都是幺半羣實例而不是原始類型的話,你會發現其實都是一個套路...
const Monoid = (x) => ({ ... }) const monoid = xs => xs.reduce( (acc, cur) => acc.concat(cur), // 使用 concat 結合 Monoid.empty() // 傳入幺元 ) monoid([Monoid(a), Monoid(b), Monoid(c)]) // 傳入幺半羣實例
因此對於思惟高度抽象的函數式來講,這樣的代碼確定是須要繼續重構精簡的~
在講解如何重構以前,先介紹兩個炒雞經常使用的不可變數據結構:List
、Map
。
顧名思義,正好對應原生的 Array
和 Object
。
由於 immutable
庫中的 List
和 Map
並無 empty
屬性和 fold
方法,因此咱們首先擴展 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
這樣一來上一節的代碼就能夠精簡成這樣:
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)
注意到 map
和 fold
這兩步操做,從邏輯上來講是一個操做,因此咱們能夠新增 foldMap
方法來結合二者。
import { List, Map } from 'immutable' const derived = { fold (empty) { return this.foldMap(x => x, empty) }, foldMap (f, empty) { return empty != null // 幺半羣中將 f 的調用放在 reduce 中,提升效率 ? this.reduce( (acc, cur, idx) => acc.concat(f(cur, idx)), empty ) : this // 在 map 中調用 f 是由於考慮到空的狀況 .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
因此最終版長這樣:
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)
下面咱們要來實現一個新容器 LazyBox
。
顧名思義,這個容器很懶...
雖然你能夠不停地用 map
給它分配任務,可是隻要你不調用 fold
方法催它執行(就像 deadline
同樣),它就死活不執行...
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)) // 沒有 fold 死活不執行 result.fold(c => c.toLowerCase()) // a
有了上一節中 LazyBox
的基礎以後,接下來咱們來建立一個新的類型 Task。
首先 Task
的構造函數能夠接收一個函數以便延遲計算,固然也能夠用 of
方法來建立實例,很天然的也有 map
、chain
、concat
、empty
等方法。
不同凡響的是它有個 fork
方法(相似於 LazyBox
中的 fold
方法,在 fork
執行前其餘函數並不會執行),以及一個 rejected
方法,相似於 Left
,忽略後續的操做。
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 // 相似 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: 2
接下來讓咱們作一個發射飛彈的程序~
const lauchMissiles = () => ( // 和 promise 很像,不過 promise 會當即執行 // 並且參數的位置也相反 new Task((rej, res) => { console.log('lauchMissiles') res('missile') }) ) // 繼續對以前的任務添加後續操做(duang~給飛彈加特技!) const app = lauchMissiles() .map(x => x + '!') // 這時才執行(發射飛彈) app.fork(showErr, showSuc)
上面的代碼乍一看好像沒啥用,只不過是把待執行的代碼用函數包起來了嘛,這還能吹上天?
還記得前面章節說到的反作用麼?雖說使用純函數是沒有反作用的,可是平常項目中有各類必須處理的反作用。
因此咱們將有反作用的代碼給包起來以後,這些新函數就都變成了純函數,這樣咱們的整個應用的代碼都是純的~,而且在代碼真正執行前(fork
前)還能夠不斷地 compose
別的函數,爲咱們的應用不斷添加各類功能,這樣一來整個應用的代碼流程都會十分的簡潔漂亮。
如下代碼作了 3 件事:
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 來改寫一下~
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}`) )
代碼一目瞭然,按照線性的前後順序完成了任務,而且在其中還能夠隨意地插入或修改需求~
Applicative Functor
提供了讓不一樣的函子(functor)互相應用的能力。
爲啥咱們須要函子的互相應用?什麼是互相應用?
先來看個簡單例子:
const add = x => y => x + y add(Box.of(2))(Box.of(3)) // NaN Box(2).map(add).inspect() // Box(y => 2 + y)
如今咱們有了一個容器,它的內部值爲局部調用(partially applied)後的函數。接着咱們想讓它應用到 Box(3)
上,最後獲得 Box(5)
的預期結果。
說到從容器中取值,那確定第一個想到 chain
方法,讓咱們來試一下:
Box(2) .chain(x => Box(3).map(add(x))) .inspect() // Box(5)
成功實現~,BUT,這種實現方法有個問題,那就是單子(Monad)的執行順序問題。
咱們這樣實現的話,就必須等 Box(2)
執行完畢後,才能對 Box(3)
進行求值。假如這是兩個異步任務,那麼徹底沒法並行執行。
別慌,吃口藥~
下面介紹下主角:ap
~:
const Box = (x) => ({ // 這裏 box 是另外一個 Box 的實例,x 是函數 ap: box => box.map(x), ... }) Box(add) // Box(y => 2 + y) ,咦?在哪兒見過? .ap(Box(2)) .ap(Box(3)) // Box(5)
運算規則
F(x).map(f) === F(f).ap(F(x)) // 這就是爲何 Box(2).map(add) === Box(add).ap(Box(2))
因爲平常編寫代碼的時候直接用 ap 的話模板代碼太多,因此通常經過使用 Lift 家族系列函數來簡化。
// F 該從哪兒來? const fakeLiftA2 = f => fx => fy => F(f).ap(fx).ap(fy) // 應用運算規則轉換一下~ 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 = ...
// 僞裝是個 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)
// 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'))
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 8
import fs from 'fs' // 詳見 4.8. const readFile = (file, enc) => ( new Task((rej, res) => ...) ) const files = ['a.js', 'b.js'] // [Task, Task],咱們獲得了一個 Task 的數組 files.map(file => readFile(file, 'utf-8'))
然而咱們想獲得的是一個包含數組的 Task([file1, file2])
,這樣就能夠調用它的 fork
方法,查看執行結果。
爲了解決這個問題,函數式編程通常用一個叫作 traverse
的方法來實現。
files .traverse(Task.of, file => readFile(file, 'utf-8')) .fork(console.error, console.log)
traverse
方法第一個參數是建立函子的函數,第二個參數是要應用在函子上的函數。
其實以上代碼有 bug
...,由於數組 Array 是沒有 traverse
方法的。沒事兒,讓咱們來實現一下~
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) ) }
看着有點兒暈?
不急,首先看代碼主體是一個 reduce
,這個很熟了,就是從左到右遍歷元素,其中的第二個參數傳遞的就是幺半羣(monoid)的單位元(empty)。
再看第一個參數,主要就是經過 applicative functor
調用 ap
方法,再將其執行結果使用 concat
方法合併到數組中。
因此最後返回的就是 Task([foo, bar])
,所以咱們能夠調用 fork
方法執行它。
天然變換就是一個函數,接受一個函子(functor),返回另外一個函子。看看代碼熟悉下~
const boxToEither = b => b.fold(Right)
這個 boxToEither
函數就是一個天然變換(nt),它將函子 Box
轉換成了另外一個函子 Either
。
那麼咱們用
Left
行不行呢?
答案是不行!
由於天然變換不只是將一個函子轉換成另外一個函子,它還知足如下規則:
nt(x).map(f) == nt(x.map(f))
舉例來講就是:
const res1 = boxToEither(Box(100)) .map(x => x * 2) const res2 = boxToEither( Box(100).map(x => x * 2) ) res1 === res2 // Right(200)
即先對函子 a
作改變再將其轉換爲函子 b
,是等價於先將函子 a
轉換爲函子 b
再作改變。
顯然,Left
並不知足這個規則。因此任何知足這個規則的函數都是天然變換。
1.例1:獲得一個數組小於等於 100 的最後一個數的兩倍的值
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) )
根據天然變換,它顯然和 first(getLargeNums(arr)).map(double)
是等價的。可是後者顯然性能好得多。
再來看一個更復雜一點兒的例子:
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, )
第二版的問題是多餘的嵌套代碼。
// 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, )
第三版的問題是多餘的重複邏輯。
// Task(Either(user)) const zero = Db.find(3) // 這其實就是天然變換 // 將 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, ) // 出錯版 const error = Db.find(2) // Task(Either(user)) // Task.rejected('not found') .chain(eitherToTask) // 這裏永遠不會被調用,被跳過了 .chain(() => console.log('hey man')) ... .fork( console.error, // not found console.log, )
同構是在數學對象之間定義的一類映射,它能揭示出在這些對象的屬性或者操做之間存在的關係。
簡單來講就是兩種不一樣類型的對象通過變形,保持結構而且不丟失數據。
具體怎麼作到的呢?
其實同構就是一對兒函數:to
和 from
,遵照如下規則:
to(from(x)) === x from(to(y)) === y
這其實說明了這兩個類型都可以無損地保存一樣的信息。
String
和 [Char]
就是同構的。// 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 方法將其轉成數組 // 這樣就能使用數組的各種方法 chars.to(str).slice(0, 3) ).concat('...') ) truncate(str) // hel...
[a]
和 Either
的同構關係// [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)
以上 to be continued...