Programming with Types —— 高階類型(Functor、Monad)

通用的 map 實現(xiàn)

map 是函數(shù)式編程中非常常見的一類接口,可以將某個函數(shù)操作應(yīng)用到一系列元素上。一個通用的 map() 實現(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

上述實現(xiàn)主要針對可迭代對象,可以將函數(shù) func(類型為 (item: T) => U)應(yīng)用給可迭代對象 iter 中的每一個元素。
為了使 map() 函數(shù)的場景更為通用,func 的參數(shù) item: T 理應(yīng)能夠接收更多類型的值,比如 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;
    }
}

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

Mapping a function over an optional value

下面是支持 Optional 類型的 map 實現(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>();
        }
    }
}

另一種簡單的通用類型 Box<T> 及其 map 實現(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));
    }
}

將類型為 (value: T) => U 的函數(shù) map 到 Box<T>,返回一個 Box<U>Box<T>T 類型的值會被取出來,傳遞給被 map 的函數(shù),再將結(jié)果放入 Box<U> 中返回。

Mapping a function over a value in a Box

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

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

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

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

還有一個 readNumber() 函數(shù)負(fù)責(zé)從文件中讀取數(shù)字。當(dāng)我們需要處理輸入數(shù)據(jù)時,有可能會遇到某些問題,比如文件不存在或者無法打開等。在上述情況下,readNumber() 函數(shù)會返回 undefined。

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

如果我們想通過 readNumber() 讀取一個數(shù)字,再將其傳遞給 square() 處理,就必須確保 readNumber() 返回的值是一個實際的數(shù)字,而不是 undefined。一種可行的方案就是借助 if 語句將 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ù)字類型的參數(shù),因而當(dāng)輸入有可能是 undefined 時,我們需要顯式地處理這類情況。但通常意義上講,代碼的分支越少,其復(fù)雜性就越低,就更易于理解和維護(hù)。
另一種實現(xiàn) process() 的方式就是,并不對 undefined 做任何處理,只是將其簡單地傳遞下去。即只讓 process() 負(fù)責(zé)數(shù)字的處理工作,error 則交給后續(xù)的其他人。

可以借助 為 sum type 實現(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);
}

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

同時 map() 是通用的函數(shù),可以直接在其他 process 函數(shù)中對更多不同類型的數(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:對于任意的泛型,比如 Box<T>,能夠通過 map() 操作將函數(shù) (value: T) => U 應(yīng)用給 Box<T>,并返回一個 Box<U>。

Functor

又或者說,F(xiàn)unctor 是支持某種 map() 函數(shù)的任意類型 H<T>。該 map() 函數(shù)接收 H<T> 作為參數(shù),一個從 TU 的函數(shù)作為另一個參數(shù),最終返回 H<U>。
以更面向?qū)ο笠稽c的形式來表現(xiàn)的話,參考如下代碼(當(dāng)然這段代碼是編譯不通過的,因為 TypeScript 不支持高階類型,如 <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ù)的 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

在前面的例子中,只有第一個函數(shù) readNumber() 有可能返回錯誤(undefined)。借助 Functor,square()stringify() 可以不經(jīng)修改地正常調(diào)用,若 readNumber() 返回 undefined,該 undefined 不會被處理,只是簡單地傳遞下去。
但是假如鏈條中的每一個函數(shù)都有可能返回錯誤,又該如何處理呢?

假設(shè)我們需要打開某個文件,讀取其內(nèi)容,再將讀取到的字符串反序列化為一個 Cat 對象。
負(fù)責(zé)打開文件的 openFile() 函數(shù)可能返回一個 Error 或者 FileHandle。比如當(dāng)文件不存在、文件被其他進(jìn)程鎖定或者用戶沒有權(quán)限讀取文件,都會導(dǎo)致返回 Error。
還需要一個 readFile() 函數(shù),接收 FileHandle 作為參數(shù),返回一個 Error 或者 String。比如有可能內(nèi)存不足導(dǎo)致文件無法被讀取,返回 Error。
最后還需要一個 deserializeCat() 函數(shù)接收 string 作為參數(shù),返回一個 Error 或者 Cat 對象。同樣的道理,string 有可能格式不符合要求,無法被反序列化為 Cat 對象,返回 Error。

所有上述函數(shù)都遵循一種“返回一個正常結(jié)果或者一個錯誤對象”的模式,其返回值類型為 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ù)并不包含具體的實現(xiàn)代碼。同時 Either 類型的實現(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)
    }
}

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

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());
}

就像在上一個例子中對 process 函數(shù)做的那樣,我們可以實現(xiàn)一個類似的 map() 函數(shù),將 readCatFromFile() 中的所有分支結(jié)構(gòu)和錯誤檢查都轉(zhuǎn)移到通用的 map() 中。
按照普遍的約定,Either<TLeft, TRight> 中的 TLeft 包含錯誤對象,map() 只會將其不做改動地傳遞下去。只有當(dāng) TRight 存在時,map() 才會對 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() 實現(xiàn)的問題在于,當(dāng)我們調(diào)用 openFile() 得到返回值 Either<Error, FileHandle>,接下來就需要一個類型為 (value: FileHandle) => string 的函數(shù)從 FileHandle 讀取文件內(nèi)容。
但是實際上的 readFile() 函數(shù)的返回類型不是 string,而是 Either<Error, string>。

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

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

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

正確的實現(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() 實現(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

對于 Box<T>,F(xiàn)unctor(map())會接收一個 Box<T> 值和一個從 TU 的函數(shù)((value: T) => U)作為參數(shù),將 T 值取出并應(yīng)用給傳入的函數(shù),最終返回 Box<U>。
Monad(bind())接收一個 Box<T> 值和一個從 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 表示對于泛型 H<T>,我們有一個 unit() 函數(shù)能夠接收 T 作為參數(shù),返回類型為 H<T> 的值;同時還有一個 bind() 函數(shù)接收 H<T> 和一個從 TH<U> 的函數(shù)作為參數(shù),返回 H<U>。
現(xiàn)實中能夠?qū)?Promise 串聯(lián)起來的 then() 方法實際上就等同于 bind(),能夠從值創(chuàng)建 Promise 的 resolve() 方法等同于 unit()。

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

參考資料

Programming with Types

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容