天天看點

進階學習4:函數式程式設計FP——函子Functor、MayBe函子、Either函子、IO函子、Folktale、Pointer函子、Monad六、函子Functor

目錄

六、函子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)
           
進階學習4:函數式程式設計FP——函子Functor、MayBe函子、Either函子、IO函子、Folktale、Pointer函子、Monad六、函子Functor

總結

  • 函數式程式設計的運算不直接操作值,而是由函子完成
  • 函子就是一個實作了 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)
           

輸出結果:

進階學習4:函數式程式設計FP——函子Functor、MayBe函子、Either函子、IO函子、Folktale、Pointer函子、Monad六、函子Functor

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']))
           

進階學習4:函數式程式設計FP——函子Functor、MayBe函子、Either函子、IO函子、Folktale、Pointer函子、Monad六、函子Functor

Task函子

Folktale Task官方文檔

https://folktale.origamitower.com/docs/v2.3.0/migrating/from-data.task/

進階學習4:函數式程式設計FP——函子Functor、MayBe函子、Either函子、IO函子、Folktale、Pointer函子、Monad六、函子Functor
//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傳回第二個結果 

進階學習4:函數式程式設計FP——函子Functor、MayBe函子、Either函子、IO函子、Folktale、Pointer函子、Monad六、函子Functor

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.拉勾網 《大前端訓練營》課程