本文是經典的Functors, Applicatives, And Monads In Pictures的Typescript翻譯版本。html
Functor/Applicative/Monad是函數式編程中的一些比較‘基礎’的概念,反正我是不認同‘基礎’這個說法的,筆者也閱讀過不少相似介紹Monad的文章,最後都不了了之,這些概念是比較難以理解的,並且平時編程實踐中也很難會接觸到這些東西。前端
後來拜讀了Functors, Applicatives, And Monads In Pictures, 不錯,好像懂了。因而本身想經過翻譯,再深刻消化消化這篇文章,這裏使用Typescript
做爲描述語言,對於前端來講會更好理解。git
有理解不正確的地方,敬請指正. 開始吧!github
這是一個簡單的值:數據庫
例如這些編程
1 // number
'string' // string
複製代碼
你們都知道怎麼將一個函數應用到這個值上面:數組
// So easy
const add3 = (val: number) => val + 3
console.log(add3(2)) // 5
複製代碼
很簡單了. 咱們來擴展一下, 讓任意的值是能夠包裝在一個**上下文(context)**當中. 如今的狀況你能夠想象一個能夠把值放進去的盒子:bash
如今你把一個函數應用到這個包裝值的時候, 根據其上下文類型你會獲得不一樣的結果. 這就是 Functor
, Applicative
, Monad
, Arrow
之類概念的基礎.app
Maybe
就是一個典型的數據類型, 它定義了兩種相關的‘上下文’, Maybe自己也是一個‘上下文’(除了值,其餘類型均可以是一個上下文?):ide
原文基於Haskell,它的Maybe類型有兩個上下文Just(藍色盒子)和None(紅色空盒子)。仿造Haskell在Typescript中咱們可使用可選類型(Maybe)
來表示:
type Maybe<T> = Just<T> | Nothing // Just 表示值‘存在’,Nothing表示空值,相似於null、undefined的概念
複製代碼
Just和Nothing的基本結構:
// 咱們只用None來取代null, 這裏咱們將None做爲一個值,而不是一個類
export class None {}
// 對應None的類型
export type Nothing = typeof None
// 判斷是不是Nothing,這裏使用Typescript的 `Type Guards`
export const isNothing = (val: any): val is Nothing => {
return val === None
}
// Just類型
export class Just<T> {
static of<T>(val: T) {
return new Just(val)
}
value: T
constructor(value: T) {
this.value = value
}
}
複製代碼
使用示例:
let a: Maybe<number>;
a = None;
a = Just.of(3);
複製代碼
說實在這個實現有點挫, 可是爲了更加貼近原文描述,暫且使用這個實現。以前考慮過的一個版本是下面這樣的, 由於沒法給它們擴展方法,就放棄了這個方案:
type Optional<T> = NonNullable<T> | nul
let a: Optional<number> = 1;
a = null;
複製代碼
很快咱們會看到對一個 Just<a>
和一個 Nothing 來講, 函數應用有何不一樣. 首先咱們來看看 Functor!
當一個值被包裝在一個上下文中時, 你就不能拿普通函數來應用了:
declare let a: Just<number>;
const add3 = (v: number) => v + 3
add3(a) // ❌ 類型「Just<number>」的參數不能賦給類型「number」的參
複製代碼
這時候, 該 fmap
出場了. fmap 翩翩而來,從容應對上下文(fmap is from the street, fmap is hip to contexts). 還有誰? fmap 知道怎樣將一個函數應用到一個包裝在上下文的值上. 你能夠對任何一個類型爲 Functor 的類型使用 fmap, 換句話說,Functor都定義了fmap.
好比說, 想一下你想把 add3 應用到 Just 2. 用 fmap:
Just.of(2).fmap(add3) // Just 5
複製代碼
💥嘭! fmap 向咱們展現了它的成果。 可是 fmap 怎麼知道如何應用該函數的呢?
在 Haskell 中 Functor
是一個類型類(typeclass)。 其定義以下:
在Typescript中, 一個Functor認爲是定義了fmap的任意類型. 看看fmap
是如何工做的:
用Typescript的函數簽名描述一下:
<Functor T>.fmap<U>(fn: (val: T) => U): <Functor U> 複製代碼
因此咱們能夠這麼作:
Just.of(2).fmap(add3) // Just 5
複製代碼
而 fmap 神奇地應用了這個函數,由於 Maybe 是一個 Functor, 它指定了 fmap 如何應用到 Just 上與 Nothing 上:
class Just<T> {
// ...
// 實現fmap
fmap<U>(fn: (val: T) => U) { return Just.of(fn(this.value)) } } class None { // None 接受任何函數都返回None static fmap(fn: any) { return None } } 複製代碼
當咱們寫 Just.of(2).fmap(add3)
時,這是幕後發生的事情:
那麼而後,就像這樣,fmap,請將 add3 應用到 Nothing 上如何?
None.fmap(add3) // Nothing
複製代碼
就像《黑客帝國》中的 Morpheus,fmap 知道都要作什麼;若是你從 Nothing 開始,那麼你會以 Nothing 結束! fmap 是禪。
如今它告訴咱們了 Maybe 數據類型存在的意義。 例如,這是在一個沒有 Maybe 的語言中處理一個數據庫記錄的方式, 好比Javascript:
let post = Post.findByID(1)
if (post != null) {
return post.title
} else {
return null
}
複製代碼
有了fmap後:
// 假設findPost返回Maybe<Article>
findPost(1).fmap(getPostTitle)
複製代碼
若是 findPost 返回一篇文章,咱們就會經過 getPostTitle 獲取其標題。 若是它返回 Nothing,咱們就也返回 Nothing! 較以前簡潔了不少對吧?
Typescript有了Optional Chaining後,處理null也能夠很簡潔:
findPost(1)?.title // 殊途同歸
複製代碼
原文還有定義了一個fmap的重載操做符版本,由於JavaScript不支持操做符重載,因此這裏簡單帶過
getPostTitle <$> findPost(1) // 使用操做符重載<$> 來簡化fmap. 等價於上面的代碼
複製代碼
再看一個示例:若是將一個函數應用到一個 Array(Haksell 中是 List)上會發生什麼?
Array 也是 functor!
[1, 2, 3].map(add3) // [4, 5, 6]. fa是Array,輸出fb也是Array,符合Functor的定義吧,因此Javascript的map就是fmap,Array就是Functor
複製代碼
好了,好了,最後一個示例:若是將一個函數應用到另外一個函數上會發生什麼?
const multiply3 = (v: number) => v * 3
const add3 = (v: number) => v + 3
add3.fmap(multiply3) // ❓
複製代碼
這是一個函數:
這是一個應用到另外一個函數上的函數:
其結果是又一個函數!
// 僅做示例,不要模仿
interface Function {
fmap<V, T, U>(this: (val: V) => T, fn: (val: T) => U): (val: V) => U
}
Function.prototype.fmap = function(fn) {
return v => fn(this(v))
}
複製代碼
因此函數也是 Functor! 對一個函數使用 fmap,其實就是函數組合(compose)! 也就是說: f.fmap(g)
等價於 compose(f, g)
經過上面的例子,能夠知道Functor其實並無那麼難以理解, 一個Functor就是:
<Functor T>.fmap(fn: (v: T) => U): <Functor U>
複製代碼
Functor會定義一個‘fmap’操做,這個fmap接受一個函數fn,fn接收的是具體的值,返回另外一個具體的值,例如上面的add3. fmap決定如何來應用fn到源Functor(a), 返回一個新的Functor(b)。 也就是fmap的源和輸出的值‘上下文’類型是同樣的。好比
Just -> fmap -> Just
Nothing -> fmap -> Nothing
Maybe -> fmap -> Maybe
Array -> fmap -> Array
如今練到二重天了。 Applicative 又提高了一個層次。
對於 Applicative,咱們的值依然和 Functor 同樣包裝在一個上下文中
不同的是,咱們將Functor中的函數(例如add3)也包裝在一個上下文中!
嗯。 咱們繼續深刻。 Applicative 並無開玩笑。不像Haskell,Typescript並無內置方式來處理Applicative。咱們能夠給須要支持Applicative的類型定義一個apply函數。apply函數知道怎麼將包裝在上下文的函數
應用到一個包裝在上下文的值
:
class None {
static apply(fn: any) {
return None;
}
}
class Just<T> {
// 使用方法重載,讓Typescript更好推斷
// 若是值和函數都是Just類型,結果也是Just類型
apply<U>(fn: Just<(a: T) => U>): Just<U>; // 若是函數是Nothing類型,結果是Nothing. // 嚴格上apply只應該接收同一個上下文類型的函數,即Just, // 由於Maybe是Typescript的Union類型,沒辦法給它擴展方法,這裏將Maybe和Just混在一塊兒了 apply<U>(fn: Nothing): Nothing; // 若是值和函數都是Maybe類型, 返回一個Maybe類型 apply<U>(fn: Maybe<(a: T) => U>): Maybe<U> { if (!isNothing(fn)) { return Just.of(fn.value(this.value)); } return None.apply(fn); } } 複製代碼
再來看看數組:
// 僅做示例
interface Array<T> {
apply<U>(fns: Array<(e: T) => U>): U[] } // 接收一個函數‘數組(上下文)’,返回一個應用了‘函數’的新的數組 Array.prototype.apply = function<T, U>(fns: Array<(e: T) => U>) { const res: U[] = [] for (const fn of fns) { this.forEach(el => res.push(fn(el))) } return res } 複製代碼
在Haskell中,使用<*>
來表示apply操做: Just (+3) <*> Just 2 == Just 5
. Typescript不支持操做符重載,因此忽略.
Just類型的Applicative應用圖解:
數組類型的Applicative應用圖解:
const num: number[] = [1, 2, 3]
console.log(num.apply([multiply2, add3]))
// [2, 4, 6, 4, 5, 6]
複製代碼
這裏有 Applicative 能作到而 Functor 不能作到的事情。 如何將一個接受兩個參數的函數應用到兩個已包裝的值上?
// 一個支持兩個參數的Curry型加法函數
const curriedAddition = (a: number) => (b: number) => a + b
Just.of(5).fmap(curriedAddition) // 返回 `Just.of((b: number) => 5 + b)`
// Ok 繼續
Just.of(4).fmap(Just.of((b: number) => 5 + b)) // ❌不行了,報錯了,Functor沒辦法處理包裝在上下文的fn
複製代碼
可是Applicative能夠:
Just.of(5).fmap(curriedAddition) // 返回 `Just.of((b: number) => 5 + b)`
// ✅噹噹噹
Just.of(3).apply(Just.of((b: number) => 5 + b)) // Just.of(8)
複製代碼
這時候Applicative 把 Functor 推到一邊。 「大人物可使用具備任意數量參數的函數,」它說。 「裝備了 <$>(fmap) 與 <*>(apply) 以後,我能夠接受具備任意個數未包裝值參數的任意函數。 而後我傳給它全部已包裝的值,而我會獲得一個已包裝的值出來! 啊啊啊啊啊!」
Just.of(3).apply(Just.of(5).fmap(curriedAddition)) // 返回 `Just.of(8)`
複製代碼
咱們重申一個Applicative的定義, 若是Functor要求實現fmap的話,Applicative就是要求實現apply,apply符合如下定義:
// 這是Functor的fmap定義
<Functor T>.fmap(fn: (v: T) => U): <Functor U>
// 這是Applicative的apply定義,和上面對比,fn變成了一個包裝在上下文的函數
<Applicative T>.apply(fn: <Applicative (v: T) => U>): <Applicative U>
複製代碼
終於練到三重天了!繼續⛽加油️
如何學習 Monad 呢:
Monad 增長了一個新的轉變。
Functor
將一個函數
應用到一個已包裝的值
上:
Applicative
將一個已包裝的函數
應用到一個已包裝的值
上:
Monad 將一個返回已包裝值的函數
應用到一個已包裝的值
上。 Monad 定義一個函數flatMap
(在 Haskell 中是使用操做符 >>=
來應用Monad,讀做「bind」)來作這個。
讓咱們來看個示例。 老搭檔 Maybe 是一個 Monad:
假設 half
是一個只適用於偶數的函數:
// 這就是一個典型的: "返回已包裝值"的函數
function half(value: number): Maybe<number> {
if (value % 2 === 0) {
return Just.of(value / 2)
}
return None
}
複製代碼
若是咱們餵給它一個已包裝的值
會怎樣?
咱們須要使用flatMap(Haskell 中的>>=)來將咱們已包裝的值塞進該函數。 這是 >>= 的照片:
如下是它的工做方式:
Just.of(3).flatMap(half) // => Nothing, Haskell中使用操做符這樣操做: Just 3 >>= half
Just.of(4).flatMap(half) // => Just 2
None.flatMap(half) // => Nothing
複製代碼
內部發生了什麼?咱們再看看flatMap的方法簽名:
// Maybe
Maybe<T>.flatMap<U>(fn: (val: T) => Maybe<U>): Maybe<U> // Array Array<T>.flatMap<U>(fn: (val: T) => U[]): U[] 複製代碼
Array是一個Monad, Javascript的Array的flatMap已經正式成爲標準, 看看它的使用示例:
const arr1 = [1, 2, 3, 4];
arr1.map(x => [x * 2]);
// [[2], [4], [6], [8]]
arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]
// only one level is flattened
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]
複製代碼
Maybe 也是一個 Monad:
class None {
static flatMap(fn: any): Nothing {
return None;
}
}
class Just<T> {
// 和上面的apply差很少
// 使用方法重載,讓Typescript更好推斷
// 若是函數返回Just類型,結果也是Just類型
flatMap<U>(fn: (a: T) => Just<U>): Just<U>; // 若是函數返回值是Nothing類型,結果是Nothing. flatMap<U>(fn: (a: T) => Nothing): Nothing; // 若是函數返回值是Maybe類型, 返回一個Maybe類型 flatMap<U>(fn: (a: T) => Maybe<U>): Maybe<U> { return fn(this.value) } } // 示例 Just.of(3).flatMap(half) // Nothing Just.of(4).flatMap(half) // Just.of(4) 複製代碼
這是與 Just 3 運做的狀況!
若是傳入一個 Nothing 就更簡單了:
你還能夠將這些調用串聯起來:
Just.of(20).flatMap(half).flatMap(half).flatMap(falf) // => Nothing
複製代碼
很炫酷哈!因此咱們如今知道Maybe既是一個Functor、Applicative,仍是一個Monad。
原文還示範了另外一個例子: IO
Monad, 咱們這裏就簡單瞭解一下
IO的簽名大概以下:
class IO<T> {
val: T
// 具體實現咱們暫不關心
flatMap(fn: (val: T) => IO<U>): IO<U>
}
複製代碼
具體來看三個函數。 getLine 沒有參數, 用來獲取用戶輸入:
function getLine(): IO<string> 複製代碼
readFile 接受一個字符串(文件名)並返回該文件的內容:
function readFile(filename: string): IO<string> 複製代碼
putStrLn 輸出字符串到控制檯:
function putStrLn(str: string): IO<void> 複製代碼
全部這三個函數都接受普通值(或無值)並返回一個已包裝的值,即IO。 咱們可使用 flatMap 將它們串聯起來!
getLine().flatMap(readFile).flatMap(putStrLn)
複製代碼
太棒了! 前排佔座來看 monad 展現!咱們不須要在取消包裝和從新包裝 IO monad 的值上浪費時間. flatMap 爲咱們作了那些工做!
Haskell 還爲 monad 提供了語法糖, 叫作 do 表達式:
foo = do
filename <- getLine
contents <- readFile filename
putStrLn contents
複製代碼
fmap
的數據類型。apply
的數據類型。flatMap
的數據類型。這三者有什麼區別呢?
函數
應用到一個已包裝的值
上。已包裝的函數
應用到已包裝的值
上。返回已包裝值的函數
應用到已包裝的值
上。綜合起來看看它們的簽名:
// 這是Functor的fmap定義
<Functor T>.fmap(fn: (v: T) => U): <Functor U>
// 這是Applicative的apply定義,和上面對比,fn變成了一個包裝在上下文的函數
<Applicative T>.apply(fn: <Applicative (v: T) => U>): <Applicative U>
// Monad的定義, 而接受一個函數, 這個函數返回一個包裝在上下文的值
<Monad T>.flatmap(fn: (v: T) => <Monad U>): <Monad U>
複製代碼
因此,親愛的朋友(我以爲咱們如今是朋友了),我想咱們都贊成 monad 是一個簡單且高明的主意(SMART IDEA(tm))。 如今你已經經過這篇指南潤溼了你的口哨,爲何不拉上 Mel Gibson 並抓住整個瓶子呢。 參閱《Haskell 趣學指南》的《來看看幾種 Monad》。 不少東西我其實掩飾了由於 Miran 深刻這方面作得很棒.
本文在原文的基礎上, 參考了下列這些翻譯版本,再次感謝這些做者: