讓我們繼續從之前的程式中抽象出更高層的東西吧!今天要講的介面是 Functor
。
Functors – map 的抽象介面
Functional programming 下每個主題的 library 都有 map 這個 function,讓你能透過一個 function 將型態 A 轉成 型態 B,
- Scala 下的 Functional 資料結構 (2)
- 如何不拋出例外的處理錯誤 (1)
- Strictness 和 Laziness (2)
- 純粹的 functional 狀態 (1)
- Purely Function 的平行化 (2)
- 能自由組合的解析器 Library (1)
跟 Monoids 的 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[_]]
型態建構子的相關說明請參考 能自由組合的解析器 Library (1)。extension 的說明請參考 純粹的 functional 狀態 (2)。
也跟 Monoids 中的 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 定律
當我們在設計像 Functor 這種抽象介面時,我們不只要考慮它該有什麼 function,還要想這個介面的定律是什麼,讓所有實作都能遵守,除此之外還有 2 個重要的點:
- 定律能幫助介面在形成語意層時,其代數性質在每個獨立實例下是合理的,舉例來說
Monoid[A]
乘Monoid[B]
的結果Monoid[(A, B)]
,因為 Monoid 定律的關係,我們可以直接知道Monoid[(A, B)]
也是具有結合律的,我們不需要了解 A 和 B 來得出這個結論。 - 我們通常依賴定律去撰寫從抽象介面中派生出去的組合器,我們之後會在看到這個的例子。
Functor 的定律跟 Purely Function 的平行化 (2) 中的 Par.map
一樣,
map(x)(a => a) == x
實作 map 其實就是隱含著 x 的結構在操作後會相同,且不會產生奇怪的副作用,例如從 List 移除第一個值、把 Some 變成 None 等等。