Cats(三):高階型別

ScalaCool發表於2018-11-26

本文由 Yison 發表在 ScalaCool 團隊部落格。

我們已經知道函式式是一種更加抽象的程式設計思維方式,它所做的事情就是高度抽象業務物件,然後對其進行組合。

談及抽象,你在 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方法:

    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)。

高階型別:用型別構造新型別

要理解高階型別,我們需要先了解什麼是「型別構造器(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]]
複製程式碼

然後,我們再用這種假設的語言特性重新實現下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 }
    }
複製程式碼

Cats(三):高階型別

相關文章