目錄
六、函子Functor
1.函子的概念
2.使用代碼示範函子
總結
3.MayBe函子
4.Either函子
5.IO函子(Input輸入Ouput輸出 函子)
6.Folktale
安裝方法
基本使用
Task函子
7.Pointer 函子
8.IO函子問題
9.Monad(單子)
六、函子Functor
1.函子的概念
學習函子是為了在函數式程式設計中把副作用控制在可控的範圍内、異常處理、異步操作等。
什麼是 Functor
容器:包含值和值的變形關系(這個變形關系就是函數)
函子:是一個特殊的容器,通過一個普通的對象來實作,該對象具有 map 方法,map 方法可以運作一個函數對值進行處理(變形關系)
2.使用代碼示範函子
我們可以通過.map進行鍊式調用
//DEMO23
//函子
//函子:是一個特殊的容器,通過一個普通的對象來實作,該對象具有 map 方法,map 方法可以運作一個函數對值進行處理(變形關系)
class Container {
//構造函數,喊隻有一個不對外公布的值,約定下劃線的成員為私有的成員
constructor (value) {
this._value = value
}
//map接收一個函數,并且處理這個值,然後還傳回一個新的函子
map (fn) {
return new Container(fn(this._value))
}
}
//map對象傳回的永遠是個新的函子,在裡面儲存了值
let result = new Container(5).map(x => x+1).map(x => x * x)
console.log(result)
//但是每次都要用new來建立看起來十分面向對象,而我們需要用函數式程式設計思想,是以改造一下代碼
class Container2 {
static of (value) {
return new Container2 (value)
}
//構造函數,喊隻有一個不對外公布的值,約定下劃線的成員為私有的成員
constructor (value) {
this._value = value
}
//map接收一個函數,并且處理這個值,然後還傳回一個新的函子
map (fn) {
return Container2.of(fn(this._value))
}
}
let result2 = Container2.of(5).map(x => x+1).map(x => x * x)
console.log(result2)
總結
- 函數式程式設計的運算不直接操作值,而是由函子完成
- 函子就是一個實作了 map 契約的對象
- 我們可以把函子想象成一個盒子,這個盒子裡封裝了一個值
- 想要處理盒子中的值,我們需要給盒子的 map 方法傳遞一個處理值的函數(純函數),由這個函數來對值進行處理
- 最終 map 方法傳回一個包含新值的盒子(函子)
3.MayBe函子
我們在程式設計的過程中可能會遇到很多錯誤,需要對這些錯誤做相應的處理 MayBe函子的作用就是可以對外部的空值情況做處理(控制副作用在允許的範圍)
//DEMO24
//MayBe函子
class MayBe {
static of (value) {
return new MayBe (value)
}
//構造函數,喊隻有一個不對外公布的值,約定下劃線的成員為私有的成員
constructor (value) {
this._value = value
}
//map接收一個函數,并且處理這個值,然後還傳回一個新的函子
map (fn) {
return this.isNothing() ? MayBe.of(null): MayBe.of(fn(this._value))
}
//判斷是否為null或者undefined
isNothing() {
return this._value === null || this._value === undefined
}
}
let result = MayBe.of('Hello World').map(x => x.toUpperCase())
console.log(result)
let result2 = MayBe.of(null).map(x => x.toUpperCase())
console.log(result2)
//但是如果有一連串的null,maybe函子能夠做處理,但是無法具體判斷是哪一步map出現的null
let result3 = MayBe.of('Hello World').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '))
console.log(result3)
4.Either函子
Either 兩者中的任何一個,類似于 if...else...的處理
異常會讓函數變的不純,Either 函子可以用來做異常處理
//DEMO25
//Either函子
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))
}
}
let r1 = Right.of(12).map(x => x + 2)
let r2 = Left.of(12).map(x => x + 2)
console.log(r1)
console.log(r2)
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
}catch (e) {
//Left函子記錄錯誤資訊
return Left.of({error : e.message})
}
}
let r = parseJSON('{name: zs}')
console.log(r)
let r_correct = parseJSON('{"name": "zs"}')
console.log(r_correct)
let r_correct2 = parseJSON('{"name": "zs"}').map(x => x.name.toUpperCase())
console.log(r_correct2)
輸出結果:
5.IO函子(Input輸入Ouput輸出 函子)
- IO 函子中的 _value 是一個函數,這裡是把函數作為值來處理
- IO 函子可以把不純的動作存儲到 _value 中,延遲執行這個不純的操作(惰性執行),包裝目前的操作純
- 把不純的操作交給調用者來處理
//DEMO26
//IO函子
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))
}
}
//調用
let r = IO.of(process).map(p => p.execPath)
console.log(r)
console.log(r._value())
//IO函子内部包裝了一些可能是不純的函數,IO本身目前執行是純的操作。我們把這個副作用延遲到了隻有調用的時候才會發生。
6.Folktale
異步任務的實作過于複雜,用 folktale 中的 Task 來示範。
folktale是一個标準的函數式程式設計庫 和 lodash、ramda 不同的是,他沒有提供很多功能函數。隻提供了一些函數式處理的操作,例如:compose、curry 等,一些函子 Task、Either、 MayBe 等
Folktale官網
https://folktale.origamitower.com/
安裝方法
npm install folktale
基本使用
//DEMO27
//folktale 中的 compose、 curry的基本使用
const {compose, curry} = require('folktale/core/lambda')
const {toUpper, first} = require('lodash/fp')
let f = curry(2 ,(x,y)=>{
return x+y
})
console.log(f(1,2))
console.log(f(1)(2))
let f2 = compose(toUpper, first)
console.log(f2(['one','two']))
Task函子
Folktale Task官方文檔
https://folktale.origamitower.com/docs/v2.3.0/migrating/from-data.task/
//DEMO28
//Task 處理異步任務
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile(filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
//隻有調用.run的時候才執行
readFile('package-lock.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run().listen({
//執行失敗
onRejected: err => {
console.log(err)
},
//執行成功
onResolved: value => {
console.log(value)
}
})
沒加那兩行.map之前傳回的是第一個結果,加了之後提取version傳回第二個結果
7.Pointer 函子
Pointed 函子是實作了 of 靜态方法的函子 of 方法是為了避免使用 new 來建立對象,更深層的含義是 of 方法用來把值放到上下文 Context(把值放到容器中,使用 map 來處理值)
其實就是前面我們用到過的帶.of的函子都是pointer函子
8.IO函子問題
//DEMO29
//IO函數問題
const fp = require('lodash/fp')
const fs = require('fs')
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))
}
}
//模拟linux 的cat指令,就是讀取檔案并列印
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 cat = fp.flowRight(print,readFile)
// IO(IO(x)) 外面的IO是print傳回的函子,裡面的IO是readFile傳回的函子
let r = cat('package-lock.json')._value()._value()
console.log(r)
現在的問題是,需要獲得print的東西需要些兩遍._value(),接下來要解決這個問題,于是會使用到Monad函子
9.Monad(單子)
Monad 函子是可以變扁的 Pointed 函子,IO(IO(x))
一個函子如果具有 join 和 of 兩個方法并遵守一些定律就是一個 Monad
//DEMO30
//Monad函子
const fp = require('lodash/fp')
const fs = require('fs')
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 () {
return this._value()
}
flatMap(fn) {
return this.map(fn).join() //傳回的是一個值
}
}
//模拟linux 的cat指令,就是讀取檔案并列印
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-lock.json').flatMap(print).join() //再調用的傳回的是值用map,傳回的是函子用flatMap。因為print傳回的是函子,是以用了flatMap
console.log(r)
//把值轉換成大寫
let r2 = readFile('package-lock.json').map(fp.toUpper).flatMap(print).join()
console.log(r2)
除了上述直接引用外的參考資料:
1.拉勾網 《大前端訓練營》課程