Applicative Functors

今天要介紹一個功能沒有 Monad 這麼強,但比較泛用的抽象介面,Applicative Functors

Monads 中,我們有寫過 traversesequence 方法,前者能使用一次迭代就把 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 的 unitmap2,而 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 是眾多延伸出的組合器方法之一,既然如此,我們也可以把 map2unit 做為介面的核心方法來用,此介面就被稱為 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 核心方法

另外一種核心方法組合是 unitapply,然後 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 之間可以相互實現,我們就延用核心方法為 map2unit 的版本,然後用 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]

D28 – Exercise answer

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。

tshine73
tshine73
文章: 50

發佈留言

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