Cats(四):Typeclass

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

上一篇介紹高階類型的文章中,咱們引出了 Typeclass 這個概念,而且演示瞭如何在 Kotlin 中模擬高階類型,以及實現了一個 Kotlin 版本的 Functor。github

若是你只是一個 Kotlin 開發者,相信你很難說服本身用這種方式進行程序設計。的確,在缺乏高階類型這種語言特性的支持下,構建 Typeclass 不是一種很天然的事情,迴歸到 Scala 和 Cats,咱們能夠慶幸沒有這種煩惱。編程

認識 Typeclass

咱們曾在《Subtyping vs Typeclasses》中具體介紹過 Typeclass,而且比較了它與熟悉的多態技術 Subtyping 之間的差別。你可能已經知曉,Typeclass 是始於 Haskell 的一種編程模式,它是一種有關特設多態的技術,能夠代替繼承來對已存的類庫擴展功能,而且無需改變源碼。安全

簡單來講,一個 Typeclass 模式,主要由三部分來組成:app

  • Typeclass 自己,一般至少是包含一個類型變量的 Trait
  • 實現了該 Typeclass 的一個實例
  • 一個封裝了 Typeclass 實例的方法,供外部調用

儘管你可能已經知道了什麼是 Typeclass,咱們仍是再經過一個例子來介紹如何定義一個 Typeclass。在以前介紹 Typeclass 的文章中咱們實現了一個 Comparable 的Typeclass,如今來回顧下它。ide

Typeclass Trait

trait Comparator[T] {
  def compare(first: T, second: T): Int
  def >=(first: T, second: T): Boolean = compare(first, second) >= 0
}
複製代碼

Comparator 包含了一個類型變量 T 來支持各類數據類型之間的比較,它能夠是 Scala 標準庫中的某個類型,也能夠是咱們本身定義的某種數據類型(固然前提是該類型擁有實現了 Comparator 的實例)。函數式編程

Typeclass Instances

咱們來定義一個數據類型 Player,表明遊戲玩家:函數

case class Player(kill: Int, death: Int, assist: Int) {
  def score = (kill + assist * 0.8) / death
}
複製代碼

如今來定義 Player 基於 Comparator 的實例:測試

object PlayerInstances {
  def ordering(o: (Player, Player) => Int) = new Comparator[Player] {
    def compare(first: Player, second: Player) = o(first, second)
  }
  implicit val mvp = ordering((p1: Player, p2: Player) => (p1.score - p2.score).toInt)
}
複製代碼

咱們發如今定義 mvp 的時候,前面帶有了 implicit 關鍵字,這個很是重要,它使得咱們後續能夠隱式調用 ordering 方法。須要注意的是,因爲咱們這裏實現的是一個基於排序的功能,因此額外定義了 mvp 來支持尋找一個 Player 列表中表現最優的對象。ui

在一般狀況下,若是咱們定義的是一個基於單個數據類型的操做(非數據間的操做),你每每會把 implicit 加在實例自己。好比《Scala with Cats》 舉了一個 Json 轉化的例子:

trait JsonWriter[A] {
  def write(value: A): Json
}

object JsonWriterInstances {
  implicit val stringWriter: JsonWriter[String] = new JsonWriter[String] {
    def write(value: String): Json = JsString(value)
  }
}
複製代碼

Typeclass Interfaces

在有了 Typeclass 實例以後,咱們就能夠對它進行封裝,而後供外部調用。因爲 Scala 存在 implicit 這種強大的語法,咱們可使用不一樣的風格來調用。首先來看看舊文中實現的方案:

object Players {
  def findMvp[T](list: List[T])(implicit ordering: Comparator[T]): T = {
    list.reduce((a, b) => if (ordering >=(a, b)) a else b)
  }
}
複製代碼

另外一種使用了 Context Bounds 的寫法是:

def findMvp[T:Comparator](list: List[T]): T = {
  list.reduce((a, b) => if (implicitly[Comparator[T]] >=(a, b)) a else b)
}
複製代碼

因爲定義了 implicit ordering,Scala 編譯器會在 Comparator[T] 中自動尋找到相關的 ordering 。因而咱們就能夠如此使用了:

import PlayerInstances.mvp

val players = List(Player(12, 3, 4), Player(5, 9, 10), Player(2, 1, 4))
Players.findMvp(players)
複製代碼

咱們見識到了 Typeclass 的神奇運用,固然你可能並非很喜歡 Players.findMvp(players) 這種語法,確實咱們還能夠對其進行改進。

如今來使用 implicit 實現另外一種方案:

import scala.language.implicitConversions
implicit def PlayerListOps[T](l: List[T]) = new PlayerList(l)

class PlayerList[T](l: List[T]) {
  def mvp(implicit ordering: Comparator[T]): T = {
    l.reduce((a, b) => if (ordering >=(a, b)) a else b)
  }
}
複製代碼

所以,咱們就能夠以下調用,可讀性獲得進一步的加強:

import PlayerInstances.mvp

val players = List(Player1(12, 3, 4), Player1(5, 9, 10), Player(2, 1, 4))
players.mvp
複製代碼

使用 Cats 的 Show

如今你應該比較熟悉 Typeclass 了。固然,關於 Typeclass 的話題還有不少,咱們能夠之後在其餘話題中再對其進行探討。接下來,你終於要跟 Cats 這個類庫打交道了。第一步,咱們就來看看如何使用 Cats 中很簡單的一個 Typeclass — Show。

引入 Show

首先來看看 Cats 中 Show 的定義,爲了便於理解,咱們能夠將其先簡化爲如下的定義(雖然這與源碼不是徹底一致):

package cats

trait Show[A] {
  def show(value: A): String
}
複製代碼

Show 支持具體類型返回一個表明自身的字符串值。也許你會說這樣子很傻,由於 Scala 在 Any 類型上都支持了 toString 方法。然而,從一個更周全的設計角度而言,這種作法是非「類型安全」的。好比如下的代碼,程序能夠經過編譯。

scala> (new {}).toString
res0: String = $anon$1@7de26db8
複製代碼

做爲對比,Show 的方案則更安全:

scala> (new {}).show
<console>:8: error: value show is not a member of AnyRef
              (new {}).show
複製代碼

相似的類型問題也存在於 Scala 標準庫的判等操做,也許你已經遇到 Option[T] 類型數據在判等時的麻煩問題,Cats 的 Eq 則提供了類型安全的解決方案,咱們稍後就會介紹它。

接下來,咱們先來看看 Showapply 方法,它支持獲取相應類型的 Show 實例:

def apply[A](implicit instance: Show[A]): Show[A] = instance
複製代碼

在調用 apply 以前,咱們須要先導入供隱式尋找的實例做用域,它存在於 cats.instances 包下,你能夠經過 cats/instances/package.scala 來查看細節。

import Cats.Show
import import cats.instances.int._

val showInt: Show[Int] = Show.apply[Int]

val int2Str: String = showInt.show(2019)
// int2Str: String = 2019
複製代碼

咱們再來看下 cats.syntax 這個包,它主要封裝了 Typeclass 實例來供外部使用,正是起到了以上 Typeclass 模式介紹中的第三個部分的做用。繼續上面代碼的例子,咱們經過引入 cats.syntax.show._ 來直接實現各類黑科技。

import cats.syntax.show._

val intShow = 2019.show
// intShow: String = 2019

val stringShow = "scala".show
// stringShow: String = scala
複製代碼

自定義 Person 類

那麼,咱們如何讓自定義的數據類型,也支持 Show 的功能呢?咱們先來定義一個 Person 類:

case class Person(name: String)
複製代碼

根據以前習得得方法,咱們能夠定義一個 Person 類型的 Show 實例:

implicit val personShow: Show[Person] = new Show[Person] {
  def show(p: Person): String = s"I am ${p.name}."
}
複製代碼

然而,做爲一個類庫 Cats 天然有更加簡單的方法。事實上,Cats 提供了兩種實現的方法:

/** creates an instance of [[Show]] using the provided function */
def show[A](f: A => String): Show[A] = new Show[A] {
  def show(a: A): String = f(a)
}

/** creates an instance of [[Show]] using object toString */
def fromToString[A]: Show[A] = new Show[A] {
  def show(a: A): String = a.toString
}
複製代碼

show方法支持傳入一個函數來自定義行爲,fromToString 則直接採用了 toString 方法。在這個例子中,咱們能夠採用 show 方法來實現須要的邏輯:

implicit val showPerson: Show[Person] = Show.show(p => s"I am ${p.name}.")
複製代碼

如今來測試下效果:

val shaw = Person("Show")
// shaw: Person = Person(Show)

shaw.show
// res1: String = Shaw
複製代碼

Eq 與判等類型安全

另外一個咱們要介紹的 Typeclass 就是 Eq。它在 Cats 中實現的套路跟 Show 是相似的,你也能夠本身聯練習下 Eq 相關的使用。這裏,咱們着重強調下判等操做中「類型安全」的重要性。

若是你編寫過必定量的 Scala 程序代碼,極可能遭遇過這樣子的陷阱:

>>> List(Some(1), Some(2), Some(3)).filter(_ == 1)
<console>:8: warning: comparing values of types Some[Int] and Int using `==' will always yield false
              List(Some(1), Some(2), Some(3)).filter(_ == 1)

res5: List[Some[Int]] = List()
複製代碼

如上所見,這段代碼經過了編譯並不會報錯,然而因爲 Option[T] 類型的存在,咱們極可能犯下這種錯誤,而且很長時間才發現問題。Cats 中的 Eq 則能夠解決這個問題。

咱們用它來作些測試:

import cats._
import cats.data._
import cats.implicits._

1 === 1
// Boolean = true

1 === "scala"
// error: type mismatch

1 =!= 2
// Boolean = true

List(Some(1), Some(2), Some(3)).filter(_ === 1)
// error: type mismatch
複製代碼

Cats 定義了 ====!= 來分別表明相等、不相等操做,它們被定義在 cats.syntax.eq 包下。實際上,它們依賴的是 Eq 特質中的 eqvneqv 方法:

/** * Returns `true` if `x` and `y` are equivalent, `false` otherwise. */
def eqv(x: A, y: A): Boolean

/** * Returns `false` if `x` and `y` are equivalent, `true` otherwise. */
def neqv(x: A, y: A): Boolean = !eqv(x, y)
複製代碼

總結

本文咱們再一次介紹了 Typeclass 模式,以及講解了 Cats 中兩個簡單的 Typeclass — ShowEq。此外,還有其餘一些簡單的 Typeclass 好比 OrderRead,建議你自行去研究,它們都採用了相似的實現方法,而且擁有良好的「類型安全」設計。

在接下來的文章裏,咱們將深刻到 Cats 中更核心的知識,好比 Monoid、Monad、Functor等等,它們是函數式編程中最通用的結構。

相關文章
相關標籤/搜索