【函式式】Monads模式初探——Functor
函子與範疇
函子(functor)是從一個範疇到另一個範疇的轉換,並且其亦可轉換/保持態射(morphism)。
一個態射是從一個範疇裡的一個值到同一個範疇裡的另一個值的變換。在貓的範疇的例子裡,一個態射好比一個盒子,能夠把黯淡無光的貓轉化為一個霓虹閃耀的貓。在型別的範疇裡(電腦科學常用的範疇),一個態射是一個把某型別轉化為另一個型別的函式。
函子是可以把貓轉化為狗的東西(不同範疇的轉換)。函子可以把暗淡的貓轉換成暗淡的狗,光彩的貓轉換成光彩的狗。函子還可以連態射都轉換過去,這樣可以把暗淡的狗轉化為光彩的狗。
圖中,下面的圈子表示所有的型別組成的範疇。裡面包括標準的String、Double和其他Scala能定義的型別。函子F是Scala裡的一個型別構造器。對於一個範疇中的任意型別T,可以把該型別置於型別構造器F[_]
F[T]
。
對函子的描述
函子本質上是範疇之間的轉換。
上圖表示範疇C到範疇D的函子。圖中,物件A和B被轉換到了範疇D中同一個物件,因此,態射g就被轉換成了一個源物件和目標物件相同的態射(不一定是單位態射),而且id_A和id_B變成了相同的態射。物件之間的轉換是用淺黃色的虛線箭頭表示,態射之間的轉換是用藍紫色的箭頭表示。
範疇之間的轉換必須在轉換時保留所有的態射才能稱為函子轉換。也就是說,如果在第一個範疇有個操作型別的函式,那麼我們也應該有個轉換後的函式能夠操作轉換後的型別。比如說,如果我們有個吧String轉換為Int的函式,那麼我們也應該能夠把F[String]轉換成F[Int],這正是map方法所提供的功能。
trait Functor[F[_]] {
def apply[A](x: A): F[A]
def map[A, B](x: F[A])(f: A => B): F[B]
}
apply方法的作用——對於任意型別A,一個函子能夠在新範疇裡構造一個型別F[A]。
map方法的作用——給定一個轉換後的型別F[A]和一個在原範疇裡的態射A => B,能夠建立一個F[B]型別的結果。也就是說,我們有個新函式能夠接受F[A],返回F[B]。
簡單的說,函子是實現了map方法的資料型別。
舉例來說,
trait Functor[F[_]] {
def fmap [A, B](x: F[A])(f: A => B): F[B]
}
object ListFunctor extends Functor[List] {
def fmap[A, B](list: List[A])(f: A => B): List[B] = list map f
}
val l1 = List(1, 2, 3)
val result = ListFunctor.fmap(l1)(i => i+1)
println(result)
// print is
// List(2, 3, 4)
函子的形象演示
假設有一個容器(container),這裡的容器可以認為是一種上下文(context)或者是為函式提供的計算上下文(computational context)。
在圖中表示為一個盒子。當一個元素被上下文包裹,我們不能簡單的對該元素應用一般的運算方法,於是需要使用map函式(Haskell中稱為fmap)。
map函式知道如何將普通函式應用到一個被上下文包裹的元素上。
函子(functor)是定義瞭如何應用map的型別類(typeclass)。
如果我們要將普通函式應用到一個有盒子上下文包裹的值,那麼我們首先需要定義一個叫Functor的資料型別,在這個資料型別中需要定義如何使用map或fmap來應用這個普通函式。
函子內部工作原理可以認為是這樣的:
- 將值從上下文盒子中解救出來
- 將外部指定的函式(+3)應用到這個值上,得到一個新的值(5)
- 再將這個新值放入到上下文盒子中
下圖顯示瞭如何將一個普通函式應用到值集合,不是單個值,而是值的集合陣列中,圖中陣列函子將陣列一個個開啟(遍歷),然後分別將普通函式應用到這些元素中,最後返回一個新的集合值。
補充:高階型別
基本泛型:
如果型別引數也是一個泛型(型別構造器):
對型別的歸納:
型別(type)是對資料的抽象,而高階型別(higher-kinded type)是對型別的抽象: