Cats(三):高階類型

本文由 Yison 發表在 ScalaCool 團隊博客。編程

咱們已經知道函數式是一種更加抽象的編程思惟方式,它所作的事情就是高度抽象業務對象,而後對其進行組合。ide

談及抽象,你在 Java 中會常常接觸到一階的參數多態,這也是咱們所熟悉的泛型。利用泛型多態,在很大程度上能夠減小大量相同的代碼。然而,當咱們須要更高階的抽象的時候,泛型也避免不了代碼冗餘。如你所知,標準庫中的ListSet等都實現了Iterable接口,它們都有相同的方法如filterremove函數式編程

如今咱們來嘗試經過泛型設計Iterable函數

trait Iterable[T] {
      def filter(p: TBoolean): Iterable[T]
      def remove(p: TBoolean): Iterable[T] = filter (x ⇒ !p(x))
    }
複製代碼

當咱們用List去實現Iterable時,因爲filterremove方法須要返回具體的容器類型,你須要從新實現這些方法:學習

trait List[T] extends Iterable[T] {
    def filter(p: TBoolean): List[T]
      override def remove(p: TBoolean): List[T] = filter (x ⇒ !p(x))
    }
複製代碼

相同的道理,Set也須要從新實現filterremove方法:ui

trait Set[T] extends Iterable[T] {
    def filter(p: TBoolean): Set[T]
      override def remove(p: TBoolean): 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]]
複製代碼

而後,咱們再用這種假設的語言特性從新實現下ListSet,會驚喜地發現冗餘的代碼消失了:

trait Iterable[T, Container[X]] {
      def filter(p: TBoolean): Container[T]
      def remove(p: TBoolean): 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表示由headtail鏈接而成的列表。

注意到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 }
    }
複製代碼

相關文章
相關標籤/搜索