文章內(nèi)容輸出來源:拉勾教育 大前端高薪訓(xùn)練營
前言
我在另一篇文章 函數(shù)式編程 – 純函數(shù)、柯里化函數(shù) 中寫到,副作用會(huì)讓一些函數(shù)變得不純,那么,我們?nèi)绾伟迅弊饔每刂圃诳煽氐姆秶鷥?nèi)呢,這就涉及到了函子的概念。
函子(Functor)
1. 什么是函子
在開始學(xué)習(xí)之前,我們先來了解什么是函子?
-
函子是一個(gè)容器,包含值和值的變形關(guān)系(即函數(shù))。 -
函子是一個(gè)特殊的容器,通過一個(gè)普通的對(duì)象來實(shí)現(xiàn),該對(duì)象具有 map 方法,map 方法可以運(yùn)行一個(gè)函數(shù)對(duì)值進(jìn)行處理(變形關(guān)系) 代碼如下(示例): // 一個(gè)容器,包裹一個(gè)值
class Container {
constructor (value) {
this._value = value // 使用_表示變量私有化
}
// map方法, 傳入變形關(guān)系(函數(shù)),將容器里面的每一個(gè)值,映射到另一個(gè)容器
map (fn) {
return Container.of(fn(this._value))
}
}
// 創(chuàng)建函子對(duì)象
let r = new Container(5)
.map(x => x + 1) // 返回新的函子對(duì)象, 在新的函子對(duì)象中保存值
.map(x => x * x )
console.log(r);
上面的代碼中,Container 是一個(gè)函子,它的map方法接受函數(shù)f作為參數(shù),然后返回一個(gè)新的函子,里面包含的值是被 fn 處理過的(fn(this._value))。 上面生成新的函子對(duì)象的時(shí)候,用了 new 命令。new 命令是面向?qū)ο缶幊痰臉?biāo)志,不符合函數(shù)式編程的思想。 -
函數(shù)式編程一般約定,函子有一個(gè)of方法,用來生成新的容器。 那么,我們接下來就用 of 方法替換掉 new 進(jìn)行改造。 代碼如下(示例): class Container {
// of 使用static,將其設(shè)置為靜態(tài)方法,可以使用 '類.類方法' 的方式調(diào)用
static of (value) {
return new Container(value)
}
...... // 下面代碼和上面的一樣,就不在此贅述了
}
// 鏈?zhǔn)骄幊?/span>
let r = Container.of(5).map(x => x + 2).map(x => x * x)
console.log(r);
-
總結(jié) 1、函數(shù)式編程的運(yùn)算不直接操作值,而是由函子完成 2、函子就是一個(gè)實(shí)現(xiàn)了 map 契約的對(duì)象 3、我們可以把函子想象成一個(gè)盒子,這個(gè)盒子里封裝了一個(gè)值 4、想要處理盒子中的值,我們需要給盒子的 map 方法傳遞一個(gè)處理值的函數(shù)(純函數(shù)),由這個(gè)函數(shù)來對(duì)值進(jìn)行處理 5、最終 map 方法返回一個(gè)包含新值的盒子(函子)
2. MayBe 函子
空值問題
-
函子接受各種函數(shù),處理容器內(nèi)部的值。但是,當(dāng)容器內(nèi)部的值是一個(gè)空值(比如null),而外部函數(shù)未必有處理空值的機(jī)制,如果傳入空值,很可能就會(huì)出錯(cuò)。 代碼如下(示例): // 值如果不小心傳入了空值(副作用)
Container.of(null) .map(x => x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null 12
解決方案
-
MayBe 函子的作用就是可以對(duì)外部的空值情況做處理(控制副作用在允許的范圍),準(zhǔn)確的說是,它的 map 方法里面設(shè)置了空值檢查。 代碼如下(示例): class Maybe {
map (fn) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value))
}
isNothing () {
return this._value == null || this._value == undefined
}
}
// 測試
let r = Maybe.of('Hello World').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '))
console.log(r);
然而,在 MayBe 函子中,我們很難確認(rèn)是哪一步產(chǎn)生的空值問題,要解決這個(gè)問題,我們就要借助下面的 Either 函子 ,去處理異常情況。
3. Either 函子
在普通的面向?qū)ο缶幊讨?,我們通常使用條件運(yùn)算語句 if…else… 進(jìn)行異常等方面的判斷。而在函數(shù)式編程中,我們是用 Either 函子 進(jìn)行表達(dá)。Either,英文意思,兩者中的任何一個(gè)。
-
Either 函子內(nèi)部有兩個(gè)值:左值(Left)和右值(Right)。右值是正常情況下使用的值,左值是右值不存在時(shí)使用的默認(rèn)值。 代碼如下(示例): // 記錄錯(cuò)誤信息, 右值不存在時(shí)使用的默認(rèn)值
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this
}
}
// 正常情況下使用的值
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Right.of(fn(this._value))
}
}
// Either 用來處理異常
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({error: e.message })
}
}
// let r = parseJSON('{ name: zs }')
// console.log(r) // 執(zhí)行 Left
let r = parseJSON('{ 'name': 'zs' }').map(x => x.name.toUpperCase())
console.log(r) // 執(zhí)行 Right
4. IO 函子
在程序運(yùn)行中,往往會(huì)有很多的函數(shù)依賴于外部環(huán)境,從而會(huì)帶來相應(yīng)的副作用,這也就是我們前面所說的不純函數(shù),在這里,我們就不多加贅述了。那么,如何可以把不純的函數(shù),讓它 “純”起來呢?為了解決這個(gè)問題,我們需要一個(gè)新的 Functor,即 IO 函子。
特性
-
IO 函子與其他函子的不同在于,IO 函子中的 _value 是一個(gè)函數(shù),把函數(shù)作為值來處理。 -
IO 函子可以把不純的動(dòng)作存儲(chǔ)到 _value(函數(shù)) 中,延遲執(zhí)行這個(gè)不純的操作(惰性執(zhí)行)。可以認(rèn)為,IO 包含的是被包裹的操作的返回值。 -
IO 函子把不純的操作交給調(diào)用者來處理。 代碼如下(示例): const { values } = require('lodash')
const fp = require('lodash/fp')
class IO {
static of (value) { //
return new IO(function () {
return value
})
}
constructor (fn) { // value 存儲(chǔ)函數(shù)
this._value = fn
}
map (fn) {
// 將傳入的 fn 進(jìn)行包裹,利用fp.flowRight() 使之柯里化
return new IO(fp.flowRight(fn, this._value))
}
}
// 調(diào)用,process:node中的進(jìn)程模塊
let r = IO.of(process).map(p => p.execPath)
// console.log(r) // IO { _value: [Function] }
console.log(r._value()); // 當(dāng)前node進(jìn)程的執(zhí)行路徑
5. Folktale
-
folktale 一個(gè)標(biāo)準(zhǔn)的函數(shù)式編程庫,和 lodash、ramda 不同的是,他沒有提供很多功能函數(shù)。 -
只提供了一些函數(shù)式處理的操作,例如:compose、curry 等,一些函子 Task、Either、 MayBe 等。 代碼如下(示例): // Folktale 函數(shù)式編程庫
const { toUpper, first } = require('lodash/fp')
const { compose, curry } = require('folktale/core/lambda')
// 第一個(gè)參數(shù)是傳入函數(shù)的參數(shù)個(gè)數(shù)
let f = curry(2, (x, y) => x + y)
console.log(f(1, 2));
console.log(f(1)(2));
let f = compose(toUpper, first)
console.log(f(['one', 'two']));
-
Task 異步執(zhí)行 Task 函子通過類似 Promise 的 resolve 的風(fēng)格來聲明一個(gè)異步流程,在下面的代碼中聲明的 readFile 函數(shù)中返回的 Task 函子 并沒有真正發(fā)起請(qǐng)求,它只聲明了一個(gè)請(qǐng)求動(dòng)作,這個(gè)動(dòng)作并沒有被執(zhí)行。 代碼如下(示例): const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile (filename) {
// 通過類似 Promise 的 resolve 的風(fēng)格來聲明一個(gè)異步流程,返回一個(gè)Task 函子
return task(resolver => {
// fs 的readFile() 執(zhí)行的是異步操作
fs.readFile(filename, 'utf-8', (err, data) => {
// 類似Promise中的resolve 和 reject
// reject用來報(bào)錯(cuò)誤信息,resolve用來獲取執(zhí)行成功的數(shù)據(jù)。
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
let version = readFile('../package.json') // 只聲明讀取文件的動(dòng)作,該動(dòng)作并未執(zhí)行
.map(split('\n')) // 通過 map 方法,添加不同的數(shù)據(jù)操作流程。
.map(find(x => x.includes('version'))) // includes() 方法用于判斷字符串是否包含指定的子字符串。
.run() // 調(diào)用 run() 觸發(fā)上面的動(dòng)作,進(jìn)行
.listen({
onRejected: err => { // 執(zhí)行失敗
console.log(err);
},
onResolved: value => { // 執(zhí)行成功
console.log(value);
}
})
console.log(version); // 'version': '1.0.0'
在上面的代碼中,Task 的異步流直到 run 之前都僅僅是「動(dòng)作」,沒有「執(zhí)行」。task 函子中提供了 run() 方法,用來觸發(fā)動(dòng)作的執(zhí)行。也就是說,執(zhí)行 run 方法之后,才會(huì)觸發(fā)上面的文件讀取,以及對(duì)文件內(nèi)容的一系列處理等操作。Task 函子中,還提供了 listen() 方法,用來監(jiān)聽事件的執(zhí)行狀態(tài)。onRejected 表示 動(dòng)作執(zhí)行失敗后,要執(zhí)行的函數(shù),onResolved 表示 動(dòng)作執(zhí)行成功后,要執(zhí)行的函數(shù)。
6. Pointed 函子
-
Pointed 函子是實(shí)現(xiàn)了 of 靜態(tài)方法的函子; -
of 方法是為了避免使用 new 來創(chuàng)建對(duì)象,更深層的含義是 of 方法用來把值放到上下文 Context(把值放到容器中,使用 map 來處理值) 代碼如下(示例): class Container {
static of (value) {
return new Container(value)
}
......
}
Contanier.of(2) .map(x => x + 5)
7. Monad(單子)
-
在使用 IO 函子的時(shí)候,如果我們寫出如下代碼: 代碼如下(示例): const fs = require('fs')
const fp = require('lodash/fp')
let readFile = function (filename) {
return new IO(function() { // 返回一個(gè)文件類型的實(shí)例
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function(x) {
return new IO(function() {
console.log(x) // 將文件內(nèi)容輸出
return x
})
}
// IO(IO(x))
// 調(diào)用 _value() 時(shí),執(zhí)行的是print 中的function
let cat = fp.flowRight(print, readFile)
// 調(diào)用
let r = cat('package.json')._value()._value()
console.log(r)
特性
-
Monad 函子是可以變扁的 Pointed 函子,IO(IO(x)) -
Monad 內(nèi)部封裝的值是一個(gè)函數(shù)(這個(gè)函數(shù)返回函子) -
一個(gè)函子如果具有 join 和 of 兩個(gè)方法并遵守一些定律就是一個(gè) Monad 代碼如下(示例): // IO Monad
const fs = require('fs')
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO(function () {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
// 通過 join 方法避免函子嵌套
join () {
return this._value()
}
// 同時(shí)調(diào)用map 和 join
flatMap (fn) {
// this.map(fn) 調(diào)用完后,返回函子
return this.map(fn).join()
}
}
// 讀取文件的內(nèi)容,并且把他們打印出來
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8') // 同步讀取文件
})
}
let print = function (x) {
return new IO(function () {
console.log(x)
return x
})
}
let r = readFile('../package.json') // 返回函子時(shí),調(diào)用faltMap; 返回值時(shí),調(diào)用map
// .map(x => x.toUpperCase())
.map(fp.toUpper)
.flatMap(print) // 返回 IO { _value: [Function] } -- 函子
.join() // 返回 map 后的文件內(nèi)容
console.log(r);
作用
- Monad 函子 主要用來解決函子嵌套的問題,通過 join 方法避免函子嵌套。
何時(shí)使用
- 當(dāng)一個(gè)函數(shù)返回一個(gè)函子的時(shí)候,需要使用 Monad。
總結(jié)
- 簡單說,Monad就是一種設(shè)計(jì)模式,表示將一個(gè)運(yùn)算過程,通過函數(shù)拆解成互相連接的多個(gè)步驟。你只要提供下一步運(yùn)算所需的函數(shù),整個(gè)運(yùn)算就會(huì)自動(dòng)進(jìn)行下去。也就是說,Monad 是將一個(gè)會(huì)返回包裹值的函數(shù)應(yīng)用到一個(gè)被包裹的值上。
參考 【函數(shù)式編程入門教程】 【異步流程與 Task 函子】 【JavaScript函數(shù)式編程 IO涵子,錯(cuò)誤處理涵子】
|