今天要介紹一個功能沒有 Monad 這麼強,但比較泛用的抽象介面,Applicative Functors,
在 Monads 中,我們有寫過 traverse
和 sequence
方法,前者能使用一次迭代就把 List[A] 轉成 Monad with List[B]
,而後者是把很多個 Monad 轉成一個 Monad,
def sequence[A](fas: List[F[A]]): F[List[A]] =
traverse(fas)(fs => fs)
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)(_ :: _))
traverse 的實現用到了 Monad 的 unit
和 map2
,而 Monad 的 map2 則是使用 flatMap
來實現,
def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] =
fa.flatMap(a => fb.map(b => f(a, b)))
其實有挺多的組合器方法只會用到 unit 和 map2 來實現,且在某些資料結構下 map2 可以不透過 flatMap 來實現,
Monad 的核心方法是 flatMap 和 unit,map2 是眾多延伸出的組合器方法之一,既然如此,我們也可以把 map2
和 unit
做為介面的核心方法來用,此介面就被稱為 Applicative Functors。
Applicative Functor 介面
Applicative 一樣繼承至 Functor,代表著所有的 Applicative 都是 Functor,然後用 map2 實現 Functor 的 map 方法,除了把上面講到的 traverse 和 sequence 搬過來以外,其它只用到 map2 和 unit 的組合器方法可以一併搬過來。
trait Applicative[F[_]] extends Functor[F]:
def unit[A](a: => A): F[A]
extension[A] (fa: F[A])
def map2[B, C](fb: F[B])(f: (A, B) => C): F[C]
def map[B](f: A => B): F[B] =
fa.map2(unit(()))((a, _) => f(a))
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)(_ :: _))
def sequence[A](fas: List[F[A]]): F[List[A]] =
traverse(fas)(fa => fa)
Exercise D28-1
實作 replicateM 和 product 方法吧。
def replicateM[A](n: Int, fa: F[A]): F[List[A]]
extension[A] (fa: F[A])
def product[B](fb: F[B]): F[(A, B)]
另一種 Applicative Functor 核心方法
另外一種核心方法組合是 unit
和 apply
,然後 map2 就可以用 apply 來實現,
trait Applicative[F[_]] extends Functor[F]:
def unit[A](a: => A): F[A]
def apply[A, B](fab: F[A => B])(fa: F[A]): F[B]
extension[A] (fa: F[A])
def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] =
apply(apply(unit(f.curried))(fa))(fb)
apply 跟 map2 之間可以相互實現,我們就延用核心方法為 map2
和 unit
的版本,然後用 map2 實現 apply。
def apply[A, B](fab: F[A => B])(fa: F[A]): F[B] =
fab.map2(fa)(_(_))
Curry (鞣製),就是將接受多個參數的函式,改為接受單一參數的方法,例如
f: (A, B) => C
,當調用f.curried
後會得到A => B => C
的方法 (function literal)。scala> val f: (Int, Double) => Double = _ * _ scala> f(5, 2.0) val res1: Double = 10.0 scala> val curriedF = f.curried scala> curriedF(5)(2.0) val res2: Double = 10.0 scala> val partialF = curriedF(5) val partialF: Double => Double = scala.Function2$$Lambda$1492/0x000000084077f840@e60c5a scala> partialF(2.0) val res3: Double = 10.0
Exercise D28-2
有了 apply 方法我們可依樣晝葫蘆輕易實現 map3, map4
,試著用 unit、apply 和 curried 來實現 map3 吧。
extension[A] (fa: F[A])
def map3[B, C, D](fb: F[B], fc: F[C])(f: (A, B, C) => D): F[D]
Monad 和 Applicative Functors 的差異
假設我們用 Option 來從 Map 資料中找東西,2 個查詢彼此獨立,通常可以輕鬆的用 map2 把結果合併起來,
val F: Applicative[Option] = ...
val depts: Map[String, String] = ...
val salaries: Map[String, Double] = ...
val o: Option[String] =
F.map2(depts.get("Steven"), salaries.get("Steven"))(
(dept, salary) => s"Steven in $dept make $salriy per year"
)
但如果我們的查詢彼此有依賴關係呢?像下面的例子,我們需要先用名字找到員工 ID,然後在用員工 ID 找到部門和薪水,此時就會需要 flatMap 或 join 來達成此需求了,
val idsByName: Map[String, Int] = ...
val departments: Map[Int, String] = ...
val salaries: Map[Int, Double] = ...
val o: Option[String] =
idsByName.get("Steven").flatMap(id =>
F.map2(departments.get(id), salaries.get(id))(
(dept, salary) => s"Steven in $dept make $salriy per year"
)
)
這裡就可以看出主要差別了,Applicative 的計算具有固定結構和單純的順序作用,而 Monad 計算可以動態的選擇先前資料,也就是說支持上下文有關係的計算。
不是所有的 Applicative Functors 都是 Monad
讓我們用 如何不拋出例外的處理錯誤 (1) 的 Either 來舉例它做為 Applicative 比 Monad 還來得好,
假設我們使用 validName、validBirthdate 和 validPhone 來檢查表格資料,每個方法回傳的型態是 Either[String, T]
,若我們想一次性的檢查所有欄位,用 map3 合併是個相當合情合理的操作,
map3(validName(field1),
validBirthdate(field2),
validPhone(field3))(WebForm(_,_,_))
但 Monad 的 map3 跟 map2 一樣,都是用 flatMap 實現,
extension[A] (fa: F[A])
def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] =
fa.flatMap(a => fb.map(b => f(a, b)))
def map3[B, C, D](fb: F[B], fc: F[C])(f: (A, B, C) => D): F[D] =
fa.flatMap(a => fb.flatMap(b => fc.map(c => f(a, b, c))))
這時候就糗了,細部拆解的話 map3 會以下面這個順序調用,
validName(field1).flatMap (f1 =>
validBirthdate(field2).flatMap (f2 =>
validPhone(field3).map(f3 =>
WebForm(f1, f2, f3)
)
這使得只要前面有任一是 Left,後續的欄位檢查就不會執行,這樣會導致我們的欄位檢查一次只能回傳一個錯誤;
那如果是用 Applicative Functor 的 map3 呢,之前的 map3 我們用 apply 和 unit 來實現,
extension[A] (fa: F[A])
def map3[B, C, D](fb: F[B], fc: F[C])(f: (A, B, C) => D): F[D] =
apply(apply(apply(unit(f.curried))(fa))(fb))(fc)
這裡沒有依賴關係,所以 3 個欄位檢查都會執行到,並且做為參數傳給 map3 合併,這也是 Either 不是 Monad 的主要原因。
Applicative Fuctor 定律
為求說明,以下用於說明 Applicative Functor 的方法宣告會將所有參數完整呈現,故會與上面看到的定義有所不同 (未使用 Scala 的 extension 功能)。
(1) Left 和 Right Identity
因為 Applicative Functor 也是繼承至 Functor 介面,所以它也是遵守著在 Functors 提到的 Functor 定律,
map(x)(a => a) == x
這隱含著其它 Applicative Functor 會用到的定律,如同我們用 map2 和 unit 來實作 map 方法那般,
def map[B](fa: F[A])(f: A => B): F[B] =
map2(fa, unit(()))((a, _) => f(a))
我們也可以把 unit 放到左邊,
def map[B](fa: F[A])(f: A => B): F[B] =
map2(unit(()), fa)((_, a) => f(a))
若把 unit 跟 fa:F[A]
當做參數丟給 map2,不管怎麼擺,其 map2 結果都是 fa,這也被稱為 Left 和 Right identity。
map2(unit(()), fa)((_, a) => a) == fa // Left identity
map2(fa, unit(()))((a, _) => a) == fa // Right identity
(2) 結合律 (Associativity Law)
首先來看一下 map3 宣告,我們在 Exercise D28-2 解答中使用了 apply 和 unit 來實現,
def map3[A, B, C, D](fa: F[A], fb: F[B], fc: F[C])(f: (A, B, C) => D): F[D] =
apply(apply(apply(unit(f.curried))(fa))(fb))(fc)
但如果我們用 map2 的觀點來看,這裡可以先合併 fa 和 fb, 然後在將其結果與 fc 合併,或者 fb 和 fc 先合併也可以,這種似曾相識感覺其實就跟 Monoid 和 Monad 的結合律一樣,
combine(a, combine(b, c)) == combine(combine(a, b), c)
compose(f, op(g, h)) == compose(compose(f, g), h)
這裡我們可以用 product
來表述結合律,回憶一下 Exercise D28-1 的解答中我們使用了 map2 來實現 product,
def product[B](fa: F[A], fb: F[B]): F[(A, B)] =
map2(fa, fb)((_, _))
然後若我們要從右邊開始配對,我們當然也能把它改成從左邊開始配對,
def assoc[A,B,C](p: (A, (B, C))): ((A, B), C) =
p match { case (a, (b, c)) => ((a, b), c)
最後用 product 來表述 Applicative Fuctor 結合律的結果如下。
product(product(fa, fb),fc) == map(product(fa, product(fb, fc)))(assoc)
(3) 自然轉換 (Natural transformation)
最後一個定律是 自然轉換,看一下以下例子,我們用 map2 來產出結果,
val F: Applicative[Option] = ...
case class Employee(name: String, id: Int)
case class Pay(rate: Double, hoursPerMonth: Double)
def format(employee: Option[Employee], pay: Option[Pay]): Option[String] =
F.map2(employee, pay)((e, p) =>
s"${e.name} 賺了 ${p.rate * p.hoursPerMonth} 元"
)
val employee: Option[Employee] = ...
val pay: Option[Pay] = ...
format(employee, pay)
這個 format 方法其實並不需要知道 Employee 或 Pay 的細節,所以 format 方法可以重構成只接收 String 和 Double 做為參數,然後 使用 map 方法取得 name 和 pay,
def format(name: Option[String], pay: Option[Double]): Option[String] =
F.map2(name, pay)((n, p) =>
s"$n 賺了 $p 元"
)
val employee: Option[Employee] = ...
val pay: Option[Pay] = ...
format(
F.map(employee)(_.name),
F.map(pay)(p => p.rate * p.hoursPerMonth)
)
這個簡單、直覺、我們常在用的方法,用正式一點方式表述的話就是自然轉換定律,
map2(a, b)(productF(f, g)) == product(map(a)(f), map(b)(g))
productF
能合併 2 個方法為 1 個方法,其方法宣告如下。
def productF[I, O, I2, O2](f: I => O, g: I2 => O2): (I, I2) => (O, O2) =
(i, i2) => (f(i), g(i2))
總結
Applicative Functors 是一個功能沒有 Monad 這麼強,但比較泛用的抽象介面,我們可以自由選擇要用 map2 或 apply 做為 Applicative Functor 的核心方法,它們都有能力將方法提升至更高的層級,讓我們有更多組合器方法可以使用,Monoid、Monad、Functor 和 Applicative Functor 的定律都大同小異,抽象出這些介面的重點是訓練我們能夠辨識出模式,然後用 functional programming 風格來實作 library。