假設我們用 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 計算可以動態的選擇先前資料,也就是說支持上下文有關係的計算。
讓我們用 Day 7 的 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 呢,昨天 Exercise D28-2 的 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 Functor 的定律吧!