Typescript版圖解Functor , Applicative 和 Monad

本文是經典的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!



Functors

當一個值被包裝在一個上下文中時, 你就不能拿普通函數來應用了:

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 怎麼知道如何應用該函數的呢?


究竟什麼是 Functor 呢?

在 Haskell 中 Functor 是一個類型類(typeclass)。 其定義以下:

在Typescript中, 一個Functor認爲是定義了fmap的任意類型. 看看fmap是如何工做的:

  1. 一個Functor類型的 fa, 例如Just 2
  2. fa 定義了一個fmap, fmap 接受一個函數fn,例如add3
  3. fmap 直到如何將fa應用到fn中, 返回一個Functor類型的 fb. fa和fb的包裝上下文類型同樣, 例如fa是Just, 那麼fb也是Just; 反之fa是Nothing,fb也是Nothing;

用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就是:

<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 又提高了一個層次。

對於 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, // 由於MaybeTypescriptUnion類型,沒辦法給它擴展方法,這裏將MaybeJust混在一塊兒了 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總結

咱們重申一個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 呢:

  1. 你要取得計算機科學博士學位。
  2. 而後把它扔掉,由於在本文你並不須要它!

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
複製代碼

總結

  1. functor 是實現了 fmap 的數據類型。
  2. applicative 是實現了 apply 的數據類型。
  3. monad 是實現了 flatMap 的數據類型。
  4. Maybe 實現了這三者,因此它是 functor、 applicative、 以及 monad。

這三者有什麼區別呢?

  1. functor: 可經過 fmap 將一個函數應用到一個已包裝的值上。
  2. applicative: 可經過 apply 將一個已包裝的函數應用到已包裝的值上。
  3. monad: 可經過 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 深刻這方面作得很棒.


擴展

本文在原文的基礎上, 參考了下列這些翻譯版本,再次感謝這些做者:

相關文章
相關標籤/搜索