Ramda 哪些讓人困惑的函數(shù)簽名規(guī)則
在我們查閱 Ramda 的文檔 時(shí), 常會(huì)見(jiàn)到一些"奇怪"的類(lèi)型簽名和用法,例如:
(Applicative f, Traversable t) => (a → f a) → t (f a) → f (t a)
或者,某一些函數(shù)"奇怪"的用法:
// R.ap can also be used as S combinator // when only two functions are passed
R.ap(R.concat, R.toUpper)('Ramda') //=> 'RamdaRAMDA'
這些"奇怪"的點(diǎn)背后投射著 Ramda "更深"一層的設(shè)計(jì)邏輯, 本文將會(huì)對(duì)此作出講解, 并闡述背后通用的函數(shù)式編程理論知識(shí)。
Ramda 為人熟知的一面?
Ramda 經(jīng)常被當(dāng)做 Lodash 的另外一個(gè)"更加FP"的替代庫(kù),相對(duì)于 Lodash,Ramda 的優(yōu)勢(shì)(之一)在于完備的柯里化與 data last 的設(shè)計(jì)帶來(lái)的便捷的管道式編程(pipe)。
舉一個(gè)簡(jiǎn)單的代碼對(duì)比示例:
- Ramda:
const myFn = R.pipe (
R.fn1,
R.fn2 ('arg1', 'arg2'),
R.fn3 ('arg3'),
R.fn4
)
- Lodash:
const myFn = (x, y) => {
const var1 = _.fn1 (x, y)
const var2 = _.fn2 (var1, 'arg1', 'arg2')
const var3 = _.fn3 (var2, 'arg3')
return _.fn4 (var3)
}
Ramda 類(lèi)型簽名?
在 Ramda 的 API 文檔中, 類(lèi)型簽名的語(yǔ)法有些"奇怪":
- add: Number → Number → Number
我們結(jié)合 Ramda 的柯里化規(guī)則, 稍加推測(cè), 可以將這個(gè)函數(shù)轉(zhuǎn)換為T(mén)ypeScript 的定義:
export function add(a: number, b: number): number;
export function add(a: number): (b: number) => number;
OK, 那為什么Ramda 的文檔不直接使用TypeScript 表達(dá)函數(shù)的類(lèi)型呢? -- 因?yàn)楦雍?jiǎn)潔!
Ramda 文檔中的類(lèi)型簽名使用的是Haskell 的語(yǔ)法, Haskell 作為一門(mén)純函數(shù)式編程語(yǔ)言, 可以很簡(jiǎn)潔地表達(dá)柯里化的語(yǔ)義, 相較之下, TypeScript 的表達(dá)方式就顯得比較臃腫。
當(dāng)然, 使用Haskell 的類(lèi)型簽名的意義不僅于此, 讓我們?cè)倏纯雌渌?奇怪"的函數(shù)類(lèi)型:
- ap:
[a → b] → [a] → [b]
Apply f => f (a → b) → f a → f b
(r → a → b) → (r → a) → (r → b)
結(jié)合文檔中的demo:
R.ap([R.multiply(2), R.add(3)], [1,2,3]); //=> [2, 4, 6, 4, 5, 6]
R.ap([R.concat('tasty '), R.toUpper], ['pizza', 'salad']); //=> ["tasty pizza", "tasty salad", "PIZZA", "SALAD"]
// R.ap can also be used as S combinator
// when only two functions are passed
R.ap(R.concat, R.toUpper)('Ramda') //=> 'RamdaRAMDA'
[a → b] → [a] → [b]我們好理解, 就是笛卡爾積;
(r → a → b) → (r → a) → (r → b)我們也能理解, 就是兩個(gè)函數(shù)的串聯(lián);
Apply f => f (a → b) → f a → f b就有點(diǎn)難理解了, 語(yǔ)法上就有些陌生, 我們先將其翻譯成TypeScript 語(yǔ)法:
:), 好吧, 這段類(lèi)型沒(méi)法簡(jiǎn)單地翻譯成TypeScript, 因?yàn)? TypeScript 不支持將 「類(lèi)型構(gòu)造器」 作為類(lèi)型參數(shù)!舉個(gè)例子:
type T<F> = F<number>;
報(bào)錯(cuò)信息如下:
Type 'F' is not generic.
在類(lèi)型簽名中F?是一個(gè)類(lèi)型構(gòu)造器, 既和Array一樣的 「返回類(lèi)型的類(lèi)型」, 然而, TypeScript 里根本無(wú)法聲明"一個(gè)類(lèi)型參數(shù)為類(lèi)型構(gòu)造器"。
正如示例中type T<F> = F<number>;?中, 我們無(wú)法告訴TypeScript, 這里的F?是一個(gè)類(lèi)型構(gòu)造器, 所以當(dāng)將number?傳入F的時(shí)候, 就報(bào)錯(cuò)了。
OK, 我們假設(shè)TypeScript 支持聲明"一個(gè)類(lèi)型參數(shù)為類(lèi)型構(gòu)造器", 讓我們?cè)賮?lái)看看Apply f => f (a → b) → f a → f b該怎么翻譯:
type AP = <F extends Appy, A, B>(f: F<((a: A) => B)>) => (fa: F<A>) => F<B>;
這里的F可以理解為一種 「上下文」, 這段類(lèi)型簽名可以先簡(jiǎn)單地理解為:
將一個(gè)包裹在上下文中的「函數(shù)」取出, 再將另一個(gè)包裹在上下文中的「值」取出, 調(diào)用函數(shù)后, 將函數(shù)的返回值重新包裹進(jìn)上下文中并返回。
這里的 「上下文」 是一個(gè)泛指, 比如我們可以將其特異化(specialize)為 Promise :
type AP = <A, B>(f: Promise<((a: A) => B)>) => (fa: Promise<A>) => Promise<B>;
const ap: AP = (f) => fa => f.then(ff => fa.then(ff));
ap? 或說(shuō) Apply 作為函數(shù)式編程中的一種常見(jiàn)抽象, 有非常重要重要的學(xué)習(xí)意義, 但其抽象的解析超出本文范圍, 在這里我們只聚焦于「是什么」, 暫不考慮「為什么」。
那么, (r → a → b) → (r → a) → (r → b)與Apply f => f (a → b) → f a → f b是什么關(guān)系?
他們之間是同父異母的關(guān)系, (r → a → b) → (r → a) → (r → b)?是對(duì)Apply f => f (a → b) → f a → f b的特異化, 正如我們對(duì)Promise 做的那樣。
函數(shù)也可以是一個(gè) 「上下文」?
答案是可以的, 我們可以將一個(gè)一元函數(shù)a -> b?理解為"一個(gè)包裹在上下文中的b?, 只不過(guò)為了獲取這個(gè)b?, 需要先傳入一個(gè)a。
先看看 Haskell 對(duì)ap 的定義:
instance Applicative ((->) r) where
(<*>) f g x = f x (g x)
替換為T(mén)ypeScript 的實(shí)現(xiàn), 我們將上面的Promise 的例子稍微修改下, 得出:
type F<A> = (a: any) => A;
type AP = <A, B>(f: F<((a: A) => B)>) => (fa: F<A>) => F<B>;
const ap: AP = f => fa => {
return (r) => f(r)(fa(r));
}
同樣的, 我們得到Apply 特異化為Array 的實(shí)現(xiàn):
type AP = <A, B>(f: Array<((a: A) => B)>) => (fa: Array<A>) => Array<B>;
const ap: AP = f => fa => {
return f.flatMap(ff => fa.map(ff));
};
綜上所述, 我們可以得出結(jié)論:
ap的類(lèi)型簽名[a → b] → [a] → [b]和(r → a → b) → (r → a) → (r → b)是Apply f => f (a → b) → f a → f b的特異化。