摘要:函數式編程一般約定,函子有一個方法,用來生成新的容器。是實現了函數并遵守一些特定規則的容器類型。定義二若為廣群,且運算還滿足結合律,即任意,有,則稱為半群。
slide 地址
四、Talk is cheap!Show me the ... MONEY!以下內容主要參考自 Professor Frisby Introduces Composable Functional JavaScript4.1.容器(Box)
假設有個函數,可以接收一個來自用戶輸入的數字字符串。我們需要對其預處理一下,去除多余空格,將其轉換為數字并加一,最后返回該值對應的字母。代碼大概長這樣...
const nextCharForNumStr = (str) => String.fromCharCode(parseInt(str.trim()) + 1) nextCharForNumStr(" 64 ") // "A"
因缺思廳,這代碼嵌套的也太緊湊了,看多了“老闊疼”,趕緊重構一把...
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...
為了這些只用一次的中間變量還要去想或者去查翻譯,也是容易“老闊疼”,再改再改~
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個小函數。
這樣一來再也不用去想中間變量的名稱到底叫什么,而且每一步做的事情十分的清晰,一眼就可以看出這段代碼在干嘛。
我們將原本的字符串變量 str 放在數組中變成了 [str],這里就像放在一個容器里一樣。
代碼是不是感覺好 door~~ 了?
不過在這里我們可以更進一步,讓我們來創建一個新的類型 Box。我們將同樣定義 map 方法,讓其實現同樣的功能。
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)4.2.Either / Maybe
假設現在有個需求:獲取對應顏色的十六進制的 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 的好處,那就是可以放心地對于這種類型的數據進行任何操作,而不是在每個函數里面小心翼翼地進行參數檢查。
4.3.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)在一起罷了。
4.4.半群(Semigroup)定義一:對于非空集合 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]))
雖然理論上對于
數字相加返回的仍然是數字(廣群)
加法滿足結合律(半群)
但是數字并沒有 concat 方法
沒事兒,讓我們來實現這個由
const Sum = (x) => ({ x, concat: ({ x: y }) => Sum(x + y), // 采用解構獲取值 inspect: () => `Sum(${x})`, }) Sum(1) .concat(Sum(2)) .inspect() // Sum(3)
除此之外,
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"], } */4.5.幺半群(Monoid)
幺半群是一個存在單位元(幺元)的半群。
半群我們都懂,不過啥是單位元?
單位元:對于半群,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
舉例來說,對于數字加法這個半群來說,0就是它的單位元,所以
對于
對于
對于
對于
對于
那么
顯然我們并不能找到這樣一個單位元 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!!!
從以上代碼可以看出幺半群比半群要安全得多,
4.6.foldMap 1.套路在上一節中幺半群的使用代碼中,如果傳入的都是幺半群實例而不是原始類型的話,你會發現其實都是一個套路...
const Monoid = (x) => ({ ... }) const monoid = xs => xs.reduce( (acc, cur) => acc.concat(cur), // 使用 concat 結合 Monoid.empty() // 傳入幺元 ) monoid([Monoid(a), Monoid(b), Monoid(c)]) // 傳入幺半群實例
所以對于思維高度抽象的函數式來說,這樣的代碼肯定是需要繼續重構精簡的~
2.List、Map在講解如何重構之前,先介紹兩個炒雞常用的不可變數據結構:List、Map。
顧名思義,正好對應原生的 Array 和 Object。
3.利用 List、Map 重構因為 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)4.利用 foldMap 重構
注意到 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)4.7.LazyBox
下面我們要來實現一個新容器 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()) // a4.8.Task 1.基本介紹
有了上一節中 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: 22.使用示例
接下來讓我們做一個發射飛彈的程序~
const lauchMissiles = () => ( // 和 promise 很像,不過 promise 會立即執行 // 而且參數的位置也相反 new Task((rej, res) => { console.log("lauchMissiles") res("missile") }) ) // 繼續對之前的任務添加后續操作(duang~給飛彈加特技!) const app = lauchMissiles() .map(x => x + "!") // 這時才執行(發射飛彈) app.fork(showErr, showSuc)3.原理意義
上面的代碼乍一看好像沒啥用,只不過是把待執行的代碼用函數包起來了嘛,這還能吹上天?
還記得前面章節說到的副作用么?雖然說使用純函數是沒有副作用的,但是日常項目中有各種必須處理的副作用。
所以我們將有副作用的代碼給包起來之后,這些新函數就都變成了純函數,這樣我們的整個應用的代碼都是純的~,并且在代碼真正執行前(fork 前)還可以不斷地 compose 別的函數,為我們的應用不斷添加各種功能,這樣一來整個應用的代碼流程都會十分的簡潔漂亮。
4.異步嵌套示例以下代碼做了 3 件事:
讀取 config1.json 中的數據
將內容中的 8 替換成 6
將新內容寫到 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 來改寫一下~
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}`) )
代碼一目了然,按照線性的先后順序完成了任務,并且在其中還可以隨意地插入或修改需求~
4.9.Applicative Functor 1.問題引入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) 進行求值。假如這是兩個異步任務,那么完全無法并行執行。
別慌,吃口藥~2.基本介紹
下面介紹下主角: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))3.Lift 家族
由于日常編寫代碼的時候直接用 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 = ...4.Lift 應用
例1
// 假裝是個 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.問題引入
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 方法第一個參數是創建函子的函數,第二個參數是要應用在函子上的函數。
2.實現其實以上代碼有 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 方法執行它。
4.11.自然變換(Natural Transformations) 1.基本概念自然變換就是一個函數,接受一個函子(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 并不滿足這個規則。所以任何滿足這個規則的函數都是自然變換。
2.應用場景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, )4.12.同構(Isomorphism)
同構是在數學對象之間定義的一類映射,它能揭示出在這些對象的屬性或者操作之間存在的關系。
簡單來說就是兩種不同類型的對象經過變形,保持結構并且不丟失數據。
具體怎么做到的呢?
其實同構就是一對兒函數:to 和 from,遵守以下規則:
to(from(x)) === x from(to(y)) === y
這其實說明了這兩個類型都能夠無損地保存同樣的信息。
1. 例如 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...2. 再來看看最多有一個參數的數組 [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)參考資料
JS函數式編程指南
Pointfree 編程風格指南
Hey Underscore, You"re Doing It Wrong!
Functional Concepts with JavaScript: Part I
Professor Frisby Introduces Composable Functional JavaScript
函數式編程入門教程
What are Functional Programming, Monad, Monoid, Applicative, Functor ??
相關文章JavaScript 函數式編程(一)
JavaScript 函數式編程(二)
JavaScript 函數式編程(三)-- 本文
JavaScript 函數式編程(四)正在醞釀...
以上 to be continued...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96872.html
摘要:它大致概述并討論了前端工程的實踐如何學習它,以及在年實踐時使用什么工具。目的是每年發布一次內容更新。前端實踐第一部分廣泛描述了前端工程的實踐。對大多數人來說,函數式編程看起來更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來了解前端開發實踐的指南。它大致概述并...
摘要:它大致概述并討論了前端工程的實踐如何學習它,以及在年實踐時使用什么工具。目的是每年發布一次內容更新。前端實踐第一部分廣泛描述了前端工程的實踐。對大多數人來說,函數式編程看起來更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來了解前端開發實踐的指南。它大致概述并...
摘要:它大致概述并討論了前端工程的實踐如何學習它,以及在年實踐時使用什么工具。目的是每年發布一次內容更新。前端實踐第一部分廣泛描述了前端工程的實踐。對大多數人來說,函數式編程看起來更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來了解前端開發實踐的指南。它大致概述并...
摘要:子類不是父類實例的問題是由類式繼承引起的。所以寄生式繼承和構造函數繼承的組合又稱為一種新的繼承方式。但是這里的寄生式繼承處理的不是對象,而是類的原型。看上去略微復雜,還得好好研究。 寄生組合式繼承(終極繼承者) 前面學習了類式繼承和構造函數繼承組合使用,也就是組合繼承,但是這種繼承方式有個問題,就是子類不是父類的實例,而子類的原型是父類的實例。子類不是父類實例的問題是由類式繼承引起的。...
摘要:函數式編程,一看這個詞,簡直就是學院派的典范。所以這期周刊,我們就重點引入的函數式編程,淺入淺出,一窺函數式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數式編程就是關于如使用通用的可復用函數進行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數式編程(Functional Programming),一...
摘要:在函數內保存數據在命令式語言中,函數內部的私有變量局部變量是不能被保存的。從程序的執行方式上來講,局部變量在棧上分配,在函數執行結束后,所占用的棧被釋放。這一點其實是破壞它的函數式特性的。 本文內容是我閱讀《JavaScript語言精髓與編程實踐》時,做的讀書筆記,周愛民老師的書寫的太深刻了! 函數式語言中的函數 首先要有一個概念:并不是一個語言支持函數,這個語言就可以叫做函數式語言。...
閱讀 1829·2021-09-22 15:55
閱讀 3521·2021-09-07 10:26
閱讀 628·2019-08-30 15:54
閱讀 684·2019-08-29 16:34
閱讀 839·2019-08-26 14:04
閱讀 3258·2019-08-26 11:47
閱讀 2134·2019-08-26 11:33
閱讀 2294·2019-08-23 15:17