本文由 Yison 發表在 ScalaCool 團隊博客。git
在上一篇介紹高階類型的文章中,咱們引出了 Typeclass 這個概念,而且演示瞭如何在 Kotlin 中模擬高階類型,以及實現了一個 Kotlin 版本的 Functor。github
若是你只是一個 Kotlin 開發者,相信你很難說服本身用這種方式進行程序設計。的確,在缺乏高階類型這種語言特性的支持下,構建 Typeclass 不是一種很天然的事情,迴歸到 Scala 和 Cats,咱們能夠慶幸沒有這種煩惱。編程
咱們曾在《Subtyping vs Typeclasses》中具體介紹過 Typeclass,而且比較了它與熟悉的多態技術 Subtyping 之間的差別。你可能已經知曉,Typeclass 是始於 Haskell 的一種編程模式,它是一種有關特設多態的技術,能夠代替繼承來對已存的類庫擴展功能,而且無需改變源碼。安全
簡單來講,一個 Typeclass 模式,主要由三部分來組成:app
儘管你可能已經知道了什麼是 Typeclass,咱們仍是再經過一個例子來介紹如何定義一個 Typeclass。在以前介紹 Typeclass 的文章中咱們實現了一個 Comparable
的Typeclass,如今來回顧下它。ide
trait Comparator[T] {
def compare(first: T, second: T): Int
def >=(first: T, second: T): Boolean = compare(first, second) >= 0
}
複製代碼
Comparator
包含了一個類型變量 T 來支持各類數據類型之間的比較,它能夠是 Scala 標準庫中的某個類型,也能夠是咱們本身定義的某種數據類型(固然前提是該類型擁有實現了 Comparator
的實例)。函數式編程
咱們來定義一個數據類型 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 實例以後,咱們就能夠對它進行封裝,而後供外部調用。因爲 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
複製代碼
如今你應該比較熟悉 Typeclass 了。固然,關於 Typeclass 的話題還有不少,咱們能夠之後在其餘話題中再對其進行探討。接下來,你終於要跟 Cats 這個類庫打交道了。第一步,咱們就來看看如何使用 Cats 中很簡單的一個 Typeclass — 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
則提供了類型安全的解決方案,咱們稍後就會介紹它。
接下來,咱們先來看看 Show
的 apply
方法,它支持獲取相應類型的 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
複製代碼
那麼,咱們如何讓自定義的數據類型,也支持 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
複製代碼
另外一個咱們要介紹的 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
特質中的 eqv
和 neqv
方法:
/** * 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 — Show
和 Eq
。此外,還有其餘一些簡單的 Typeclass 好比 Order
、Read
,建議你自行去研究,它們都採用了相似的實現方法,而且擁有良好的「類型安全」設計。
在接下來的文章裏,咱們將深刻到 Cats 中更核心的知識,好比 Monoid、Monad、Functor等等,它們是函數式編程中最通用的結構。