本文由 Yison 發表在 ScalaCool 團隊博客。編程
咱們已經知道函數式是一種更加抽象的編程思惟方式,它所作的事情就是高度抽象業務對象,而後對其進行組合。ide
談及抽象,你在 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
方法:ui
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)。this
要理解高階類型,咱們須要先了解什麼是「類型構造器(type constructor)」。探討到構造器,你應該很是熟悉所謂的「值構造器(value constructor)」。spa
不少狀況下,值構造器能夠是一個函數,咱們能夠給一個函數傳遞一個值參數,從而構造出一個新的值。就像這樣子:scala
(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]
複製代碼
這樣就能夠寫出更加抽象和強大的代碼。
相信你已經有點感受到高階類型的強大之處,那麼它有哪些具體應用呢?
事實上,在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版本的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 }
}
複製代碼