Programming with Types —— 高階類(lèi)型(Functor李皇、Monad)

通用的 map 實(shí)現(xiàn)

map 是函數(shù)式編程中非常常見(jiàn)的一類(lèi)接口,可以將某個(gè)函數(shù)操作應(yīng)用到一系列元素上聪建。一個(gè)通用的 map() 實(shí)現(xiàn)如下:

function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):
    IterableIterator<U> {
    for (const value of iter) {
        yield func(value);
    }
}
Map over Iterable

上述實(shí)現(xiàn)主要針對(duì)可迭代對(duì)象,可以將函數(shù) func(類(lèi)型為 (item: T) => U)應(yīng)用給可迭代對(duì)象 iter 中的每一個(gè)元素茫陆。
為了使 map() 函數(shù)的場(chǎng)景更為通用金麸,func 的參數(shù) item: T 理應(yīng)能夠接收更多類(lèi)型的值,比如 Option<T>簿盅。

class Optional<T> {
    private value: T | undefined;
    private assigned: boolean;

    constructor(value?: T) {
        if (value) {
            this.value = value;
            this.assigned = true;
        } else {
            this.value = undefined;
            this.assigned = false;
        }
    }

    hasValue(): boolean {
        return this.assigned;
    }

    getValue(): T {
        if (!this.assigned) throw Error();
        return <T>this.value;
    }
}

從邏輯上看挥下,將一個(gè)類(lèi)型為 (value: T) => U 的函數(shù) map 到 Optional<T> 類(lèi)型揍魂,如果該 Optional<T> 里面包含一個(gè)類(lèi)型為 T 的值,則返回值應(yīng)該是包含 UOptional<U> 類(lèi)型棚瘟;若 Optional<T> 并不包含任何值现斋,則 map 操作應(yīng)該返回一個(gè)空的 Optional<U>

Mapping a function over an optional value

下面是支持 Optional 類(lèi)型的 map 實(shí)現(xiàn):

namespace Optional {
    export function map<T, U>(
        optional: Optional<T>, func: (value: T) => U): Optional<U> {
        if (optional.hasValue()) {
            return new Optional<U>(func(optional.getValue()));
        } else {
            return new Optional<U>();
        }
    }
}

另一種簡(jiǎn)單的通用類(lèi)型 Box<T> 及其 map 實(shí)現(xiàn):

class Box<T> {
    value: T;

    constructor(value: T) {
        this.value = value
    }
}

namespace Box {
    export function map<T, U>(
        box: Box<T>, func: (value: T) => U): Box<U> {
        return new Box<U>(func(box.value));
    }
}

將類(lèi)型為 (value: T) => U 的函數(shù) map 到 Box<T>偎蘸,返回一個(gè) Box<U>庄蹋。Box<T>T 類(lèi)型的值會(huì)被取出來(lái),傳遞給被 map 的函數(shù)禀苦,再將結(jié)果放入 Box<U> 中返回蔓肯。

Mapping a function over a value in a Box

處理結(jié)果 or 傳遞錯(cuò)誤

假設(shè)我們需要實(shí)現(xiàn)一個(gè) square() 函數(shù)來(lái)計(jì)算某個(gè)數(shù)字的平方,以及一個(gè) stringify 函數(shù)將數(shù)字轉(zhuǎn)換為字符串振乏。示例如下:

function square(value: number): number {
    return value ** 2;
}

function stringify(value: number): string {
    return value.toString();
}

還有一個(gè) readNumber() 函數(shù)負(fù)責(zé)從文件中讀取數(shù)字蔗包。當(dāng)我們需要處理輸入數(shù)據(jù)時(shí),有可能會(huì)遇到某些問(wèn)題慧邮,比如文件不存在或者無(wú)法打開(kāi)等调限。在上述情況下,readNumber() 函數(shù)會(huì)返回 undefined误澳。

function readNumber(): number | undefined {
    /* Implementation omitted */
    return 2
}

如果我們想通過(guò) readNumber() 讀取一個(gè)數(shù)字耻矮,再將其傳遞給 square() 處理,就必須確保 readNumber() 返回的值是一個(gè)實(shí)際的數(shù)字忆谓,而不是 undefined裆装。一種可行的方案就是借助 if 語(yǔ)句將 number | undefined 轉(zhuǎn)換為 number

function process(): string | undefined {
    let value: number | undefined = readNumber();
    if (value == undefined) return undefined;
    return stringify(square(value));
}

square() 接收數(shù)字類(lèi)型的參數(shù)倡缠,因而當(dāng)輸入有可能是 undefined 時(shí)哨免,我們需要顯式地處理這類(lèi)情況。但通常意義上講昙沦,代碼的分支越少琢唾,其復(fù)雜性就越低,就更易于理解和維護(hù)盾饮。
另一種實(shí)現(xiàn) process() 的方式就是采桃,并不對(duì) undefined 做任何處理,只是將其簡(jiǎn)單地傳遞下去丘损。即只讓 process() 負(fù)責(zé)數(shù)字的處理工作普办,error 則交給后續(xù)的其他人。

可以借助 為 sum type 實(shí)現(xiàn)的 map()

namespace SumType {
    export function map<T, U>(
        value: T | undefined, func: (value: T) => U): U | undefined {
        if (value == undefined) {
            return undefined;
        } else {
            return func(value);
        }
    }
}

function process(): string | undefined {
    let value: number | undefined = readNumber();
    let squaredValue: number | undefined = SumType.map(value, square)
    return SumType.map(squaredValue, stringify);
}

此時(shí)的 process() 實(shí)現(xiàn)不再包含分支邏輯徘钥。將 number | undefined 解包為 number 并對(duì) underfined 進(jìn)行檢查的操作由 map() 負(fù)責(zé)衔蹲。

同時(shí) map() 是通用的函數(shù),可以直接在其他 process 函數(shù)中對(duì)更多不同類(lèi)型的數(shù)據(jù)使用(如 string | undefined)吏饿,減少重復(fù)代碼踪危。

版本一(不借助 map):

function squareSumType(value: number | undefined): number | undefined {
    if (value == undefined) return undefined;
    return square(value);
}

function squareBox(box: Box<number>): Box<number> {
    return new Box(square(box.value))
}

function stringifySumType(value: number | undefined): string | undefined {
    if (value == undefined) return undefined;
    return stringify(value)
}

function stringifyBox(box: Box<number>): Box<string> {
    return new Box(stringify(box.value));
}

版本二(借助 map):

let x: number | undefined = 1;
let y: Box<number> = new Box(42);
console.log(SumType.map(x, stringify))
console.log(Box.map(y, stringify))
console.log(SumType.map(x, square))
console.log(Box.map(y, square))

Functor 定義

Functor:對(duì)于任意的泛型,比如 Box<T>猪落,能夠通過(guò) map() 操作將函數(shù) (value: T) => U 應(yīng)用給 Box<T>贞远,并返回一個(gè) Box<U>

Functor

又或者說(shuō)笨忌,F(xiàn)unctor 是支持某種 map() 函數(shù)的任意類(lèi)型 H<T>蓝仲。該 map() 函數(shù)接收 H<T> 作為參數(shù),一個(gè)從 TU 的函數(shù)作為另一個(gè)參數(shù)官疲,最終返回 H<U>袱结。
以更面向?qū)ο笠稽c(diǎn)的形式來(lái)表現(xiàn)的話,參考如下代碼(當(dāng)然這段代碼是編譯不通過(guò)的途凫,因?yàn)?TypeScript 不支持高階類(lèi)型垢夹,如 <H<T>>):

interface Functor<H<T>> {
    map<U>(func: (value: T) => U): H<U>;
}

class Box<T> implements Functor<Box<T>> {
    value: T;

    constructor(value: T) {
        this.value = value;
    }

    map<U>(func: (value: T) => U): Box<U> {
        return new Box(func(this.value));
    }
}

Functors for functions

實(shí)際上還存在針對(duì)函數(shù)的 Functor。

Functor for function
namespace Function {
    export function map<T, U>(
        f: (arg1: T, arg2: T) => T, func: (value: T) => U)
        : (arg1: T, arg2: T) => U {
        return (arg1: T, arg2: T) => func(f(arg1, arg2));
    }
}

function add(x: number, y: number): number {
    return x + y;
}

function stringify(value: number): string {
    return value.toString();
}

const result: string = Function.map(add, stringify)(40, 2);
console.log(result)

Monads

在前面的例子中维费,只有第一個(gè)函數(shù) readNumber() 有可能返回錯(cuò)誤(undefined)果元。借助 Functor,square()stringify() 可以不經(jīng)修改地正常調(diào)用犀盟,若 readNumber() 返回 undefined而晒,該 undefined 不會(huì)被處理,只是簡(jiǎn)單地傳遞下去阅畴。
但是假如鏈條中的每一個(gè)函數(shù)都有可能返回錯(cuò)誤倡怎,又該如何處理呢?

假設(shè)我們需要打開(kāi)某個(gè)文件贱枣,讀取其內(nèi)容监署,再將讀取到的字符串反序列化為一個(gè) Cat 對(duì)象。
負(fù)責(zé)打開(kāi)文件的 openFile() 函數(shù)可能返回一個(gè) Error 或者 FileHandle冯事。比如當(dāng)文件不存在焦匈、文件被其他進(jìn)程鎖定或者用戶(hù)沒(méi)有權(quán)限讀取文件,都會(huì)導(dǎo)致返回 Error昵仅。
還需要一個(gè) readFile() 函數(shù)缓熟,接收 FileHandle 作為參數(shù),返回一個(gè) Error 或者 String摔笤。比如有可能內(nèi)存不足導(dǎo)致文件無(wú)法被讀取够滑,返回 Error
最后還需要一個(gè) deserializeCat() 函數(shù)接收 string 作為參數(shù)吕世,返回一個(gè) Error 或者 Cat 對(duì)象彰触。同樣的道理,string 有可能格式不符合要求命辖,無(wú)法被反序列化為 Cat 對(duì)象况毅,返回 Error分蓖。

所有上述函數(shù)都遵循一種“返回一個(gè)正常結(jié)果或者一個(gè)錯(cuò)誤對(duì)象”的模式,其返回值類(lèi)型為 Either<Error, ...>尔许。

declare function openFile(path: string): Either<Error, FileHandle>;
declare function readFile(handle: FileHandle): Either<Error, string>;
declare function deserializeCat(serializedCat: string): Either<Error, Cat>;

只是為了方便舉例么鹤,上述函數(shù)并不包含具體的實(shí)現(xiàn)代碼。同時(shí) Either 類(lèi)型的實(shí)現(xiàn)如下:

class Either<TLeft, TRight> {
    private readonly value: TLeft | TRight;
    private readonly left: boolean;

    private constructor(value: TLeft | TRight, left: boolean) {
        this.value = value;
        this.left = left;
    }

    isLeft(): boolean {
        return this.left;
    }

    getLeft(): TLeft {
        if (!this.isLeft()) throw new Error();
        return <TLeft>this.value;
    }

    isRight(): boolean {
        return !this.left;
    }

    getRight(): TRight {
        if (this.isRight()) throw new Error();
        return <TRight>this.value;
    }

    static makeLeft<TLeft, TRight>(value: TLeft) {
        return new Either<TLeft, TRight>(value, true);
    }

    static makeRight<TLeft, TRight>(value: TRight) {
        return new Either<TLeft, TRight>(value, false)
    }
}

最終將上述各個(gè)函數(shù)連接起來(lái)的 process 函數(shù)類(lèi)似下面這樣:

function readCatFromFile(path: string): Either<Error, Cat> {
    let handle: Either<Error, FileHandle> = openFile(path);
    if (handle.isLeft()) return Either.makeLeft(handle.getLeft());
    let content: Either<Error, string> = readFile(handle.getRight());
    if (content.isLeft()) return Either.makeLeft(content.getLeft());
    return deserializeCat(content.getRight());
}

就像在上一個(gè)例子中對(duì) process 函數(shù)做的那樣味廊,我們可以實(shí)現(xiàn)一個(gè)類(lèi)似的 map() 函數(shù)蒸甜,將 readCatFromFile() 中的所有分支結(jié)構(gòu)和錯(cuò)誤檢查都轉(zhuǎn)移到通用的 map() 中。
按照普遍的約定余佛,Either<TLeft, TRight> 中的 TLeft 包含錯(cuò)誤對(duì)象柠新,map() 只會(huì)將其不做改動(dòng)地傳遞下去。只有當(dāng) TRight 存在時(shí)辉巡,map() 才會(huì)對(duì) Either 應(yīng)用給定的函數(shù)恨憎。

namespace Either {
    export function map<TLeft, TRight, URight>(
        value: Either<TLeft, TRight>,
        func: (value: TRight) => URight): Either<TLeft, URight> {
        if (value.isLeft()) return Either.makeLeft(value.getLeft());
        return Either.makeRight(func(value.getRight()));
    }
}

上述 map() 實(shí)現(xiàn)的問(wèn)題在于,當(dāng)我們調(diào)用 openFile() 得到返回值 Either<Error, FileHandle>郊楣,接下來(lái)就需要一個(gè)類(lèi)型為 (value: FileHandle) => string 的函數(shù)從 FileHandle 讀取文件內(nèi)容框咙。
但是實(shí)際上的 readFile() 函數(shù)的返回類(lèi)型不是 string,而是 Either<Error, string>痢甘。

當(dāng)我們調(diào)用

let handle: Either<Error, FileHandle> = openFile(path);
let content: Either<Error, string> = Either.map(handle, readFile);

會(huì)導(dǎo)致爆出 Type 'Either<Error, Either<Error, string>>' is not assignable to type 'Either<Error, string>'. 錯(cuò)誤喇嘱。

正確的實(shí)現(xiàn)應(yīng)該是如下形式的 bind() 方法:

namespace Either {
    export function bind<TLeft, TRight, URight>(
        value: Either<TLeft, TRight>,
        func: (value: TRight) => Either<TLeft, URight>
    ): Either<TLeft, URight> {
        if (value.isLeft()) return Either.makeLeft(value.getLeft());
        return func(value.getRight());
    }
}

借助 bind() 實(shí)現(xiàn)的 readCatFromFile() 函數(shù):

function readCatFromFile(path: string): Either<Error, Cat> {
    let handle: Either<Error, FileHandle> = openFile(path);
    let content: Either<Error, string> = Either.bind(handle, readFile);
    return Either.bind(content, deserializeCat);
}

Functor vs Monad

對(duì)于 Box<T>,F(xiàn)unctor(map())會(huì)接收一個(gè) Box<T> 值和一個(gè)從 TU 的函數(shù)((value: T) => U)作為參數(shù)塞栅,將 T 值取出并應(yīng)用給傳入的函數(shù)者铜,最終返回 Box<U>
Monad(bind())接收一個(gè) Box<T> 值和一個(gè)從 TBox<U> 的函數(shù)((value: T) => Box<U>)作為參數(shù)放椰,將 T 值取出并應(yīng)用給傳入的函數(shù)作烟,最終返回 Box<U>

Functor vs Monad
class Box<T> {
    value: T;

    constructor(value: T) {
        this.value = value
    }
}


namespace Box {
    export function map<T, U>(
        box: Box<T>, func: (value: T) => U): Box<U> {
        return new Box<U>(func(box.value));
    }

    export function bind<T, U>(
        box: Box<T>, func: (value: T) => Box<U>): Box<U> {
        return func(box.value);
    }
}


function stringify(value: number): string {
    return value.toString();
}

const s: Box<string> = Box.map(new Box(42), stringify);
console.log(s)
// => Box { value: '42' }


function boxify(value: number): Box<string> {
    return new Box(value.toString());
}

const b: Box<string> = Box.bind(new Box(42), boxify);
console.log(b)
// => Box { value: '42' }

Monad 定義

Monad 表示對(duì)于泛型 H<T>砾医,我們有一個(gè) unit() 函數(shù)能夠接收 T 作為參數(shù)拿撩,返回類(lèi)型為 H<T> 的值;同時(shí)還有一個(gè) bind() 函數(shù)接收 H<T> 和一個(gè)從 TH<U> 的函數(shù)作為參數(shù)如蚜,返回 H<U>压恒。
現(xiàn)實(shí)中能夠?qū)?Promise 串聯(lián)起來(lái)的 then() 方法實(shí)際上就等同于 bind(),能夠從值創(chuàng)建 Promise 的 resolve() 方法等同于 unit()错邦。

借助 Monad探赫,函數(shù)調(diào)用序列可以表示為一條抽離了數(shù)據(jù)管理、控制流程或副作用的管道撬呢。

參考資料

Programming with Types

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末伦吠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毛仪,老刑警劉巖搁嗓,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異箱靴,居然都是意外死亡谱姓,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)刨晴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人路翻,你說(shuō)我怎么就攤上這事狈癞。” “怎么了茂契?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵蝶桶,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我掉冶,道長(zhǎng)真竖,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任厌小,我火速辦了婚禮恢共,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘璧亚。我一直安慰自己讨韭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布癣蟋。 她就那樣靜靜地躺著透硝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疯搅。 梳的紋絲不亂的頭發(fā)上濒生,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音幔欧,去河邊找鬼罪治。 笑死,一個(gè)胖子當(dāng)著我的面吹牛礁蔗,可吹牛的內(nèi)容都是我干的规阀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瘦麸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼谁撼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤厉碟,失蹤者是張志新(化名)和其女友劉穎喊巍,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體箍鼓,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崭参,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了款咖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崇猫。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖泻肯,靈堂內(nèi)的尸體忽然破棺而出佳励,到底是詐尸還是另有隱情,我是刑警寧澤富腊,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布坏逢,位于F島的核電站,受9級(jí)特大地震影響赘被,放射性物質(zhì)發(fā)生泄漏是整。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一民假、第九天 我趴在偏房一處隱蔽的房頂上張望浮入。 院中可真熱鬧,春花似錦羊异、人聲如沸舵盈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)秽晚。三九已至,卻和暖如春筒愚,著一層夾襖步出監(jiān)牢的瞬間赴蝇,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工巢掺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留句伶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓陆淀,卻偏偏與公主長(zhǎng)得像考余,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子轧苫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容