通用的 map 實現
map 是函數式編程中非常常見的一類接口,可以將某個函數操作應用到一系列元素上。一個通用的 map()
實現如下:
function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):
IterableIterator<U> {
for (const value of iter) {
yield func(value);
}
}
上述實現主要針對可迭代對象,可以將函數 func
(類型爲 (item: T) => U
)應用給可迭代對象 iter
中的每一個元素。
爲了使 map()
函數的場景更爲通用,func
的參數 item: T
理應能夠接收更多類型的值,比如 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
的函數 map 到 Optional<T>
類型,如果該 Optional<T>
裏面包含一個類型爲 T
的值,則返回值應該是包含 U
的 Optional<U>
類型;若 Optional<T>
並不包含任何值,則 map 操作應該返回一個空的 Optional<U>
。
下面是支持 Optional 類型的 map 實現:
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 實現:
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
的函數 map 到 Box<T>
,返回一個 Box<U>
。Box<T>
中 T
類型的值會被取出來,傳遞給被 map 的函數,再將結果放入 Box<U>
中返回。
處理結果 or 傳遞錯誤
假設我們需要實現一個 square()
函數來計算某個數字的平方,以及一個 stringify
函數將數字轉換爲字符串。示例如下:
function square(value: number): number {
return value ** 2;
}
function stringify(value: number): string {
return value.toString();
}
還有一個 readNumber()
函數負責從文件中讀取數字。當我們需要處理輸入數據時,有可能會遇到某些問題,比如文件不存在或者無法打開等。在上述情況下,readNumber()
函數會返回 undefined
。
function readNumber(): number | undefined {
/* Implementation omitted */
return 2
}
如果我們想通過 readNumber()
讀取一個數字,再將其傳遞給 square()
處理,就必須確保 readNumber()
返回的值是一個實際的數字,而不是 undefined
。一種可行的方案就是藉助 if
語句將 number | undefined
轉換爲 number
。
function process(): string | undefined {
let value: number | undefined = readNumber();
if (value == undefined) return undefined;
return stringify(square(value));
}
square()
接收數字類型的參數,因而當輸入有可能是 undefined
時,我們需要顯式地處理這類情況。但通常意義上講,代碼的分支越少,其複雜性就越低,就更易於理解和維護。
另一種實現 process()
的方式就是,並不對 undefined
做任何處理,只是將其簡單地傳遞下去。即只讓 process()
負責數字的處理工作,error 則交給後續的其他人。
可以藉助 爲 sum type 實現的 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()
實現不再包含分支邏輯。將 number | undefined
解包爲 number
並對 underfined
進行檢查的操作由 map()
負責。
同時 map()
是通用的函數,可以直接在其他 process 函數中對更多不同類型的數據使用(如 string | undefined
),減少重複代碼。
版本一(不借助 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()
操作將函數 (value: T) => U
應用給 Box<T>
,並返回一個 Box<U>
。
又或者說,Functor 是支持某種 map()
函數的任意類型 H<T>
。該 map()
函數接收 H<T>
作爲參數,一個從 T
到 U
的函數作爲另一個參數,最終返回 H<U>
。
以更面向對象一點的形式來表現的話,參考如下代碼(當然這段代碼是編譯不通過的,因爲 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
實際上還存在針對函數的 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
在前面的例子中,只有第一個函數 readNumber()
有可能返回錯誤(undefined
)。藉助 Functor,square()
和 stringify()
可以不經修改地正常調用,若 readNumber()
返回 undefined
,該 undefined
不會被處理,只是簡單地傳遞下去。
但是假如鏈條中的每一個函數都有可能返回錯誤,又該如何處理呢?
假設我們需要打開某個文件,讀取其內容,再將讀取到的字符串反序列化爲一個 Cat
對象。
負責打開文件的 openFile()
函數可能返回一個 Error
或者 FileHandle
。比如當文件不存在、文件被其他進程鎖定或者用戶沒有權限讀取文件,都會導致返回 Error
。
還需要一個 readFile()
函數,接收 FileHandle
作爲參數,返回一個 Error
或者 String
。比如有可能內存不足導致文件無法被讀取,返回 Error
。
最後還需要一個 deserializeCat()
函數接收 string
作爲參數,返回一個 Error
或者 Cat
對象。同樣的道理,string 有可能格式不符合要求,無法被反序列化爲 Cat
對象,返回 Error
。
所有上述函數都遵循一種“返回一個正常結果或者一個錯誤對象”的模式,其返回值類型爲 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>;
只是爲了方便舉例,上述函數並不包含具體的實現代碼。同時 Either
類型的實現如下:
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)
}
}
最終將上述各個函數連接起來的 process 函數類似下面這樣:
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
函數做的那樣,我們可以實現一個類似的 map()
函數,將 readCatFromFile()
中的所有分支結構和錯誤檢查都轉移到通用的 map()
中。
按照普遍的約定,Either<TLeft, TRight>
中的 TLeft
包含錯誤對象,map()
只會將其不做改動地傳遞下去。只有當 TRight
存在時,map()
纔會對 Either
應用給定的函數。
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()
實現的問題在於,當我們調用 openFile()
得到返回值 Either<Error, FileHandle>
,接下來就需要一個類型爲 (value: FileHandle) => string
的函數從 FileHandle
讀取文件內容。
但是實際上的 readFile()
函數的返回類型不是 string
,而是 Either<Error, string>
。
當我們調用
let handle: Either<Error, FileHandle> = openFile(path);
let content: Either<Error, string> = Either.map(handle, readFile);
會導致爆出 Type 'Either<Error, Either<Error, string>>' is not assignable to type 'Either<Error, string>'.
錯誤。
正確的實現應該是如下形式的 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()
實現的 readCatFromFile()
函數:
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>
,Functor(map()
)會接收一個 Box<T>
值和一個從 T
到 U
的函數((value: T) => U
)作爲參數,將 T
值取出並應用給傳入的函數,最終返回 Box<U>
。
Monad(bind()
)接收一個 Box<T>
值和一個從 T
到 Box<U>
的函數((value: T) => Box<U>
)作爲參數,將 T
值取出並應用給傳入的函數,最終返回 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()
函數能夠接收 T
作爲參數,返回類型爲 H<T>
的值;同時還有一個 bind()
函數接收 H<T>
和一個從 T
到 H<U>
的函數作爲參數,返回 H<U>
。
現實中能夠將 Promise 串聯起來的 then()
方法實際上就等同於 bind()
,能夠從值創建 Promise 的 resolve()
方法等同於 unit()
。
藉助 Monad,函數調用序列可以表示爲一條抽離了數據管理、控制流程或副作用的管道。