通用的 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);
}
}

上述實現(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)該是包含 U 的 Optional<U> 類型;若 Optional<T> 并不包含任何值,則 map 操作應(yīng)該返回一個空的 Optional<U>。

下面是支持 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> 中返回。

處理結(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>。

又或者說,F(xiàn)unctor 是支持某種 map() 函數(shù)的任意類型 H<T>。該 map() 函數(shù)接收 H<T> 作為參數(shù),一個從 T 到 U 的函數(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。

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> 值和一個從 T 到 U 的函數(shù)((value: T) => U)作為參數(shù),將 T 值取出并應(yīng)用給傳入的函數(shù),最終返回 Box<U>。
Monad(bind())接收一個 Box<T> 值和一個從 T 到 Box<U> 的函數(shù)((value: T) => Box<U>)作為參數(shù),將 T 值取出并應(yīng)用給傳入的函數(shù),最終返回 Box<U>。

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> 和一個從 T 到 H<U> 的函數(shù)作為參數(shù),返回 H<U>。
現(xiàn)實中能夠?qū)?Promise 串聯(lián)起來的 then() 方法實際上就等同于 bind(),能夠從值創(chuàng)建 Promise 的 resolve() 方法等同于 unit()。
借助 Monad,函數(shù)調(diào)用序列可以表示為一條抽離了數(shù)據(jù)管理、控制流程或副作用的管道。