本文由 Yison 發表在 ScalaCool 團隊部落格。
我們已經知道函式式是一種更加抽象的程式設計思維方式,它所做的事情就是高度抽象業務物件,然後對其進行組合。
談及抽象,你在 Java 中會經常接觸到一階的引數多型,這也是我們所熟悉的泛型。利用泛型多型,在很大程度上可以減少大量相同的程式碼。然而,當我們需要更高階的抽象的時候,泛型也避免不了程式碼冗餘。如你所知,標準庫中的List
、Set
等都實現了Iterable
介面,它們都有相同的方法如filter
、remove
。
現在我們來嘗試通過泛型設計Iterable
:
trait Iterable[T] {
def filter(p: T ⇒ Boolean): Iterable[T]
def remove(p: T ⇒ Boolean): Iterable[T] = filter (x ⇒ !p(x))
}
複製程式碼
當我們用List去實現Iterable
時,由於filter
、remove
方法需要返回具體的容器型別,你需要重新實現這些方法:
trait List[T] extends Iterable[T] {
def filter(p: T ⇒ Boolean): List[T]
override def remove(p: T ⇒ Boolean): List[T] = filter (x ⇒ !p(x))
}
複製程式碼
相同的道理,Set
也需要重新實現filter
和remove
方法:
trait Set[T] extends Iterable[T] {
def filter(p: T ⇒ Boolean): Set[T]
override def remove(p: T ⇒ Boolean): Set[T] = filter (x ⇒ !p(x))
}
複製程式碼
如上所示,這種利用一階引數多型的技術依舊存在程式碼冗餘。現在我們停下來想一想,假使型別也能像函式一樣支援高階,也就是可以通過型別來創造新的型別,那麼多階型別就可以上升到更高的抽象,從而進一步消除冗餘的程式碼—這便是我們接下來要談論的高階型別(higher-order kind)。
高階型別:用型別構造新型別
要理解高階型別,我們需要先了解什麼是「型別構造器(type constructor)」。探討到構造器,你應該非常熟悉所謂的「值構造器(value constructor)」。
很多情況下,值構造器可以是一個函式,我們可以給一個函式傳遞一個值引數,從而構造出一個新的值。就像這樣子:
(x: Int) => x
複製程式碼
作為比較,型別構造器就可以為傳遞一個型別變數,然後構造出一個新的型別。比如List[T]
,當我們傳入Int時,就可以產出List[Int]
型別。
在上述例子中,值建構函式的返回結果x是具體的值,List[T]
傳入型別變數後,也是具體的型別(如List[Int]
)。當我們討論「一階」概念的時候,具體的值或資訊就是構造的結果。
因此,我們可以進一步推導:
- 一階值構造器:通過傳入一個具體的值,然後構造出另一個具體的值;
- 一階型別構造器:通過傳入一個具體的型別變數,然而構造出另一個具體的型別。
在理解了上述的概念之後,我們就更好地理解高階函式了。它突破了一階值構造器,可以支援傳入一個值構造器,或者返回另一個值構造器。如:
{ (x: Int => Int) => x(1) }
{ x: Int => {y: Int => x + y} }
複製程式碼
同樣的道理,高階型別就可以支援傳入構造器變數,或是構造出另一個型別構造器。我們可以定義一種型別構造器Container
,然後將其作為另一個型別構造器Iterable的型別變數:
trait Iterable[T, Container[X]]
複製程式碼
然後,我們再用這種假設的語言特性重新實現下List
、Set
,會驚喜地發現冗餘的程式碼消失了:
trait Iterable[T, Container[X]] {
def filter(p: T ⇒ Boolean): Container[T]
def remove(p: T ⇒ Boolean): Container[T] = filter (x ⇒ !p(x))
}
trait List[T] extends Iterable[T, List]
trait Set[T] extends Iterable[T, Set]
複製程式碼
這樣就可以寫出更加抽象和強大的程式碼。
高階型別和Typeclass
相信你已經有點感覺到高階型別的強大之處,那麼它有哪些具體應用呢?
事實上,在Haskell中高階型別特性天然了催生了這門語言中一項非常強大的語言特性—Typeclass。接下來我們用Scala這門語言,來實現一個很常見的Typeclass例子—Functor(函子)。
關於什麼是Typeclass可以閱讀 scala.cool/2017/09/sub…
函子:高階型別之間的對映
當你第一次接觸到“函子”這個概念的時候,可能會有點怵,因為函數語言程式設計非常近似數學,更準確地說,函數語言程式設計思想的背後理論,是一套被叫做範疇論的學科。
範疇論是抽象地處理數學結構以及結構之間聯絡的一門數學理論,以抽象的方法來處理數學概念,將這些概念形式化成一組組的「物件」及「態射」。
然而,你千萬不要被這些術語嚇到。因為本質上他們是非常容易理解的東西。我們先來看看上面提到的“對映”,你肯定在學習集合論的時候遇到過它。在程式設計中,函式其實就可以看成是具體型別之間的對映關係。那麼,當我們來理解函子的時候,其實只要將其看成是高階型別的引數型別之間的對映,就很容易理解了。
下面我們來用Scala定義一個高階型別Functor:
trait Functor[F[_]] {
def fmap[A, B](fa: F[A], f: A => B): F[B]
}
複製程式碼
現在來分析下Functor的實現:
-
Functor支援傳入型別變數F,這也是一個高階型別;
-
Functor中實現了一個fmap方法,它接收一個型別為
F[A]
的引數變數fa
,以及一個函式f,通過它我們可以把fa中的元素型別A對映為B,即fmap
方法返回的結果型別為F[B]
。
如果你仔細思考,會發現Functor的應用非常廣泛。舉個例子,我們希望將一個List[Int]
中的元素都轉化為字串,下面我們就來看看在Scala中,如何讓List[T]
整合Functor的功能:
implicit val ListFunctor = new Functor[List] {
def fmap[A,B](f:A=>B): List[A] => List[B] = list =>list map f
}
複製程式碼
在Kotlin中用擴充套件方法實現Typeclass
現在我們打算做個挑戰——實現一個Kotlin版本的Functor。然而Kotlin不支援高階型別,像前文例子Functor[F[_]]
中的F[_]
在Kotlin中並沒有與之對應概念。
慶幸的是Jeremy Yallop和Leo White曾經在論文《Lightweight higher-kinded polymorphism》中闡述了一種模擬高階型別的方法。
我們以Functor為例來看看這種方法是如何模擬出高階型別的。
interface Kind<out F, out A>
interface Functor<F> {
fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B>
}
複製程式碼
首先我們定義了型別 Kind<out F, out A>來表示型別構造器F應用型別引數A產生的型別,當然F實際上並不能攜帶型別引數。
接下來我們看看這個高階型別如何應用到具體型別中,為此我們自定義了List型別,如下:
sealed class List<out A> : Kind<List.K, A> {
object K
}
object Nil : List<Nothing>()
data class Cons<A>(val head: A, val tail: List<A>) : List<A>()
複製程式碼
List
有兩個狀態構成,一個是Nil
代表空的列表,另一個Cons
表示由head
和tail
連線而成的列表。
注意到List實現了Kind<List.K, A>
,代入上面Kind的定義,我們得到List<A>
是型別構造器List.K
應用型別引數A
之後得到的型別。由此我們就可以用List.K
代表List
這個高階型別。
回到Functor的例子,我們很容易設計List
的Functor例項:
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
inline fun <A> Kind<List.K, A>.unwrap(): List<A> =
this as List<A>
object ListFunctor: Functor<List.K> {
override def fun <A, B> Kind<List.K, A>.map(f: (A) -> B): Kind<List.K, B> {
return when (this) {
is Cons -> {
val t = this.tail.map(f).unwrap()
Cons<B>(f(this.head), t)
}
else -> Nil
}
}
}
複製程式碼
如上面例子所示,我們就構造出了List
型別的Functor例項。現在還差最後的關鍵一步:如何使用這個例項。
眾所周知,Kotlin無法將object內部的擴充套件方法直接import進來,也就是說以下的程式碼是不行的:
import ListFunctor.*
Cons(1, Nil).map{ it + 1}
複製程式碼
我們沒法將定義在object裡的擴充套件方法直接import,慶幸的是Kotlin中的receiver機制可以將object中的成員引入作用域,所以我們只需要使用run
函式,就可以使用這個例項。
ListFunctor.run {
Cons(1, Nil).map { it + 1 }
}
複製程式碼