通用的 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);
}
}
上述實(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)該是包含 U
的 Optional<U>
類(lèi)型棚瘟;若 Optional<T>
并不包含任何值现斋,則 map 操作應(yīng)該返回一個(gè)空的 Optional<U>
。
下面是支持 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>
中返回蔓肯。
處理結(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>
。
又或者說(shuō)笨忌,F(xiàn)unctor 是支持某種 map()
函數(shù)的任意類(lèi)型 H<T>
蓝仲。該 map()
函數(shù)接收 H<T>
作為參數(shù),一個(gè)從 T
到 U
的函數(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。
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è)從 T
到 U
的函數(shù)((value: T) => U
)作為參數(shù)塞栅,將 T
值取出并應(yīng)用給傳入的函數(shù)者铜,最終返回 Box<U>
。
Monad(bind()
)接收一個(gè) Box<T>
值和一個(gè)從 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 表示對(duì)于泛型 H<T>
砾医,我們有一個(gè) unit()
函數(shù)能夠接收 T
作為參數(shù)拿撩,返回類(lèi)型為 H<T>
的值;同時(shí)還有一個(gè) bind()
函數(shù)接收 H<T>
和一個(gè)從 T
到 H<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ù)管理、控制流程或副作用的管道撬呢。