Monads

Monads – flatMap 和 unit 的抽象介面

在介紹 Monad 之前,先聊聊一個之前有在 OptionParser 下用到的 function map2,它能用一個 high-order function,把 2 個 Parser/Option 舉起來之後,轉成新的給你,

def map2[A, B, C](oa: Option[A], ob: Option[B])(f: (A, B) => C): Option[C] =
    oa.flatMap(a => ob.map(b => f(a, b)))   

def map2[A, B, C](pa: Parser[A], pb: Parser[B])(f: (A, B) => C): Parser[C] =
    pa.flatMap(a => pb.map(b => f(a, b)))

若我們要一般化這些動作的話,就是 flatMap 和 map,然而若有 flatMap,map 就可以用 flatMap + unit 來實作,

def map[A, B](pa: Parser[A])(f: A => B): Parser[B] =
  pa.flatMap(a => unit(f(a)))

所以我們就可以用 flatMap 和 unit 當做抽象介面裡的核心 function,此介面被稱為 Monad,

trait Monad[F[_]] extends Functor[F]:
  def unit[A](a: => A): F[A]

  extension[A] (fa: F[A])
    def flatMap[B](f: A => F[B]): F[B]

    def map[B](f: A => B): F[B] =
      fa.flatMap(a => unit(f(a)))

    def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] =
      fa.flatMap(a => fb.map(b => f(a, b)))

Monad 的概念也是來自於 Category Theory,它被定義為 Functor 下的其中一個結構,所以我們才會看到 Monad 繼承了 Functor,代表了所有的 Monad 都是 Functor,但不是所有的 Functor 都是 Monad,

我們用來實作一個 Parser 的 Monad 吧!

  def parserMonad[P[+_]](p: Parsers[P]): Monad[P] = new:
    def unit[A](a: => A) = p.succeed(a)

    extension[A] (fa: P[A])
      def flatMap[B](f: A => P[B]): P[B] =
        p.flatMap(fa)(f)

Exercise D26-1

實作一個 Option 和 List 的 Monad(跟 MonoidsFoldable[List] 一樣,這裡用 given 關鍵字讓它變成具有上下文關係)。

given listMonad: Monad[List] with
given optionMonad: Monad[Option] with

Monad 組合器方法

Monoids 一樣,我們可以設計多樣化組合器 function 來操作 Monad,例如 product function,它能將 2 個 Monad 配對成一個 Monad(放在 Monad 的 extension 底下);

def product[B](fb: F[B]): F[(A, B)] =
  fa.map2(fb)((_, _))

而另一個組合器例子可以來看看 traverse function,它能夠僅使用一次迭代就把 List[A] 轉成 Monad with List[B]

def traverse[A, B](as: List[A])(f: A => F[B]): F[List[B]] =
  as.foldRight(unit(List[B]()))((a, acc) => f(a).map2(acc)(_ :: _))

Exercise D26-2

來嘗試實作 sequence function 吧,它的實作程式跟 traverse 很類似。

def sequence[A](fas: List[F[A]]): F[List[A]]

我們在 能自由組合的解析器 Library (1) 的 Parser 下定義過 listOfN function,

def listOfN[A](n: Int, Parser[A]): Parser[List[A]]

它讓我們重覆一個 Parser n 次,然後得到相對應長度 List 的 Parser,這裡我們也可以把它一般化並放到 Monad 介面底下,但這次給它個更通俗的名稱吧!


Exercise D26-3

實作 replicateM。

def replicateM[A](n: Int, fa: F[A]): F[List[A]]

截至目前為止看到的組合器 function 只是一小部份的範例,最重要的是想像和動手下去玩,明天繼續介紹 Monad 的定律。

Monads 定律 1 – 結合律 (Associative Law)

假設我們有個 Item 和 Order 類別,

case class Item(name: String, price: Double)
case class Order(item: Item, quantity: Int)

在來用 for-comprehensions 來產生 Option[Order]

  val order: Option[Order] = for
    name <- Some("iphone 15")
    price <- Some(45900.0)
    quantity <- Some(1)
  yield Order(Item(name, price), quantity)

Scala for-comprehensions 的說明請參考 如何不拋出例外的處理錯誤 (2)

有時候我們也會以這樣的方式來產生 Option[Order]

  val optItem: Option[Item] = for
    name <- Some("iphone 15")
    price <- Some(45900.0)
  yield Item(name, price)

  val order: Option[Order] = for
    item <- optItem
    quantity <- Some(1)
  yield Order(item, quantity)

這 2 個 Order 都是透過 for-comprehensions 而產生,乍看起來這 2 個 Order 相同是非常合理的,

但若我們將它們細部拆解會得到下面的調用順序,

// case 1
  Some("iphone 15").flatMap(name =>
    Some(45900.0).flatMap(price =>
      Some(1).map(quantity =>
        Order(Item(name, price), quantity))))

// case 2
  Some("iphone 15").flatMap(name =>
    Some(45900.0).map(price =>
      Item(name, price)).flatMap(item =>
      Some(1).map(quantity =>
        Order(item, quantity))))
}

這程式看起來就很不一樣了,那為什麼我們對第一個版本的 for-comprehensions 會有這種 2 個 Order 是一樣的感覺呢?

因為 flatMap 也是遵守著 Associative property (結合律)

x.flatMap(f).flatMap(g) == x.flatMap(a => f(a).flatMap(g))

這定律可用在所有 Monad 上。

證明看看阿你

這裡我們一樣可以用 Option 來證明上面的定律為真,我們要做的就是把 x 替換成 Some 或 None 來看最後是不是相等,

Option 型態的說明請參考 如何不拋出例外的處理錯誤 (1)

None

None.flatMap(f).flatMap(g) == None.flatMap(a => f(a).flatMap(g))

None 的 flatMap 還是 None,所以簡化等號 2 邊後會得到,

None == None

所以 None 符合該定律。

Some

若 x 等於包裹著任一值 v 的 Some(v)

首先是替換 x,

Some(v).flatMap(f).flatMap(g) == Some(v).flatMap(a => f(a).flatMap(g)) 

因為 f 是接受一個參數並回傳 Option,所以我們可以簡化 Some(v).flatMap(f) 這個算式為 f(v)

f(v).flatMap(g) == (a => f(a).flatMap(g))(v)

最後等號右邊的算式可以分 2 個部份看,左半部是 higher-order function (a => f(a).flatMap(g)),右半部是 (v),意思就是把 v 當做左半部的 function 的參數傳入,

有點難理解的話請參考以下程式,除了可以把 x 宣告成 function 外,也能用匿名的方式直接調用

scala> val x: (Int => Int) = ((a: Int) => a + 1)

scala> x(5)
val res1: Int = 6

scala> ((a: Int) => a + 1)(5)
val res2: Int = 6

但因為 function (a => f(a).flatMap(g)) 也沒做什麼特別的事,所以我們可以繼續簡化,

f(v).flatMap(g) == f(v).flatMap(g))

完整推導過程如下:

x.flatMap(f).flatMap(g) == x.flatMap(a => f(a).flatMap(g))

Some(v).flatMap(f).flatMap(g) == Some(v).flatMap(a => f(a).flatMap(g)) 

f(v).flatMap(g) == (a => f(a).flatMap(g))(v)

f(v).flatMap(g) == f(v).flatMap(g))

最後可以觀察到 Some 也符合該定律。

Kleisli Composition

這個結合律真的很難讓人理解,好消息是我們有個方法可以讓它好理解些,

如果我們不從 Monad 的值 F[A] 來理解,而是從像 A => F[B] 的 Monad function 來思考,然後組合多個 function 成結合律的結果,

def compose[A, B, C](f: A => F[B], g: B => F[C]): A => F[C] =
    a => f(a).flatMap(g)

這個 A => F[B] 方法也被稱為 Kleisli arrow,而這個組合方法被稱為 Kleisli Composition

最後 Monad 的結合律可以寫成跟 Monoid 一樣比較好理解。

compose(compose(f, g), h) == compose(f, compose(g, h))

Monads 定律 2 – 同一律 (identity laws)

Identity laws 就像 Monoid 的 empty 被稱為 identity element (單位元素) 一樣,Monad 的 identity element 是 unit

def unit[A](a: => A): F[A]

任何跟 unit 組合的 function 會是相同的,這個通常會用 2 個定律來表示:Left identityRight identity

compose(unit, f) == f // Left identity
compose(f, unit) == f // Right identity

Exercise D27-1

map, unit, join 是另外一組 Monad 的一元組合器方法,試著用 flatMap 來實作 join 吧,它主要是用來移除最外那層的 F。

def join[A](ffa: F[F[A]]): F[A]

總結

這 3 天介紹了 Functor 和 Monad 介面,就是 map 和 flatMap 方法的抽象介面,這些也都是從數學 Category Theory 來的概念,整個抽象的過程都大同小異,有核心 API、定律和延伸出的組合器方法,雖然得花點時間理解,但能用 Monad 讓資料支援 map, flatMap 操作是件很棒的事情。


D26 – Exercise answer

D27 – Exercise answer

tshine73
tshine73
文章: 50

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *