Monads – flatMap 和 unit 的抽象介面
在介紹 Monad 之前,先聊聊一個之前有在 Option 和 Parser 下用到的 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(跟 Monoids 的 Foldable[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 identity 和 Right 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 操作是件很棒的事情。