讓我們繼續從之前的程式中抽象出更高層的東西吧!今天要講的介面是 Functor
。
從 Day 3 開始,每個主題下的 library 都有 map 這個 function,讓你能透過一個 function 將型態 A 轉成 型態 B,
跟 Day 24 - Monoids (3) 的 Foldable 結構一樣,map 動作其實也不管你操作的是什麼資料型態,所以我們可以把這個動作抽象成 Functor 這個介面,
Functor 也是來自於數學,它是 Category Theory 中的映射 (mapping) 。
trait Functor[F[_]]:
extension[A] (fa: F[A])
def map[B](f: A => B): F[B]
Functor[F[_]]
型態建構子的相關說明請參考 Day 19。extension 的說明請參考 Day 14。
也跟 Foldable 一樣,我們用 F[_]
來表示型態建構子,然後也用 List 來實作看看吧。
object Functor:
given listFunctor: Functor[List] with
extension[A] (as: List[A])
def map[B](f: A => B): List[B] = as.map(f)
有了 Functor,其實我們可以從中延伸很多操作,這裡要介紹其中之二,distribute 和 codistribute;
如果我們有 F[(A, B)]
,F 是個 Functor,我們可以 distribute (分散) 這個 Functor 成 (F[A], F[B])
,
extension[A, B] (fab: F[(A, B)])
def distribute: (F[A], F[B]) =
(fab.map(_(0)), fab.map(_(1)))
舉個具體一點的例子就是 List[(A, B)]
,如果我們 distribute 它,我們會得到 2 個相同長度的 List[A]
和 List[B]
,這個操作在 List 中被稱為 unzip,所以我們就是用 distribute function 來一般化 unzip,讓所有 Functor 都能做到相同的事情;
在來是跟 distribute 做的事有點相反,稱為 codistribute,
extension[A, B] (e: Either[F[A], F[B]])
def codistribute: F[Either[A, B]] =
e match
case Left(fa) => fa.map(Left(_))
case Right(fb) => fb.map(Right(_))
最後完整的 Functor 程式如下:
trait Functor[F[_]]:
extension[A] (fa: F[A])
def map[B](f: A => B): F[B]
extension[A, B] (fab: F[(A, B)])
def distribute: (F[A], F[B]) =
(fab.map(_(0)), fab.map(_(1)))
extension[A, B] (e: Either[F[A], F[B]])
def codistribute: F[Either[A, B]] =
e match
case Left(fa) => fa.map(Left(_))
case Right(fb) => fb.map(Right(_))
當我們在設計像 Functor 這種抽象介面時,我們不只要考慮它該有什麼 function,還要想這個介面的定律是什麼,讓所有實作都能遵守,除此之外還有 2 個重要的點:
Monoid[A]
乘 Monoid[B]
的結果 Monoid[(A, B)]
,因為 Monoid 定律的關係,我們可以直接知道 Monoid[(A, B)]
也是具有結合律的,我們不需要了解 A 和 B 來得出這個結論。Functor 的定律跟 Day 18 - Purely Function 的平行化 (4) 中的 Par.map
一樣,
map(x)(a => a) == x
實作 map 其實就是隱含著 x 的結構在操作後會相同,且不會產生奇怪的副作用,例如從 List 移除第一個值、把 Some 變成 None 等等。
明天開始要介紹第 3 個純粹代數資料結構 Monad 啦!