最近在學習Cats,發現Scala-with-cats這本書寫的不錯,因此有想法將其翻譯成中文,另外也能夠在翻譯的過程當中加深理解,另外我會對每部份內容建議須要瞭解的程度,幫助你們更好的學習總體內容(有部份內容理解起來比較晦澀且不經常使用,瞭解便可),同時我也將相關的練習代碼放到github上了,你們可下載參考:scala-with-cats,有翻譯不許確的地方,也但願你們能指正🙏。git
本篇內容主要爲Type class與Implicit,這應該算是學習Cats須要瞭解的最基礎的內容。es6
學習程度:須要徹底掌握
Type class模式主要由3個模塊組成:github
Type class 能夠當作一個接口或者 API,用於定義咱們想要實現功能。在 Cats 中,Type class至關於至少帶有一個類型參數的 trait。好比如下定義表明將一個值轉換爲Json的行爲:編程
// Define a very simple JSON AST 聲明一些簡單的JSON AST sealed trait Json final case class JsObject(get: Map[String, Json]) extends Json final case class JsString(get: String) extends Json final case class JsNumber(get: Double) extends Json case object JsNull extends Json // The "serialize to JSON" behaviour is encoded in this trait 序列話JSON方法定義在這個Trait裏 trait JsonWriter[A] { def write(value: A): Json }
這個例子中 JsonWriter
就是咱們定義的一個 Type class,上述代碼中還包含Json類型相關的代碼。post
Type Class instance 就是特定類型的 Type Class實現,包括Scala的基本類型以及咱們本身定義的類型。學習
在Scala中,Type Class instance能夠經過實現對應類型Type Class來聲明,並用 implicit 這個關鍵詞進行標記:this
final case class Person(name: String, email: String) object JsonWriterInstances { implicit val stringWriter: JsonWriter[String] = new JsonWriter[String] { def write(value: String): Json = JsString(value) } implicit val personWriter: JsonWriter[Person] = new JsonWriter[Person] { def write(value: Person): Json = JsObject(Map( "name" -> JsString(value.name), "email" -> JsString(value.email) )) } // etc... }
Type Class Interface 包含對咱們想要對外部暴露的功能。interfaces是指接受 type class instance 做爲 implicit
參數的泛型方法。scala
一般有兩種方式去建立 interface:翻譯
建立 interface 最簡單的方式就是將方法放在一個單例object中:調試
object Json { def toJson[A](value: A)(implicit w: JsonWriter[A]): Json = w.write(value) }
在使用以前,咱們須要導入咱們所需的 type class instances,而後就能夠調用相關的方法:
import JsonWriterInstances._ Json.toJson(Person("Dave", "dave@example.com")) // res4: Json = JsObject(Map(name -> JsString(Dave), email -> JsString(dave@example.com)))
這裏咱們並無指定對應的 implicit parameters,可是編譯器會幫咱們在導入的 type class instances 中尋找一個跟相應類型匹配的 type class instance,並插入對應的位置:
Json.toJson(Person("Dave", "dave@example.com"))(personWriter)
咱們也可使用擴展方法使已存在的類型擁有 interface methods,在 Cats 中將此稱爲 「syntax」:
object JsonSyntax { implicit class JsonWriterOps[A](value: A) { def toJson(implicit w: JsonWriter[A]): Json = w.write(value) } }
使用 interface syntax 以前,咱們除了導入它自己之外,還需導入咱們所需的 type class instance:
import JsonWriterInstances._ import JsonSyntax._ Person("Dave", "dave@example.com").toJson // res6: Json = JsObject(Map(name -> JsString(Dave), email -> JsString(dave@example.com)))
一樣,編譯器會自動幫咱們尋找所需implicit parameters並插入對應的位置:
Person("Dave", "dave@example.com").toJson(personWriter)
Scala 標準庫提供了一個泛型的 type class interface 叫作 implicitly,它的聲明很是簡單:
def implicitly[A](implicit value: A): A = value
它接收一個 implicit 參數並返回該參數,咱們可使用 implicitly 調用 implicit scope 中的任意值,只須要指定對應的類型無需其餘操做,便能獲得對應的 instance 對象。
import JsonWriterInstances._ // import JsonWriterInstances._ implicitly[JsonWriter[String]] // res8: JsonWriter[String] = JsonWriterInstances$$anon$1@38563298
在 Cats 中,大多數 type class 都提供了其餘方式去調用對應的 instance。可是在代碼調試過程當中,implicitly 有着很大的用處。咱們能夠在代碼中插入implicitly 相關代碼,來確保編譯器能找到對應的 type class instance(若無對應的 type class instance 則編譯的時候會抱錯)以及不會出現歧義性(好比 implicit scope 存在兩個相同的 type class instance)。
對於 Scala 來講,使用 type class 就得跟 implicit values 和 implicit parameters 打交道,爲了更好的使用它,咱們須要瞭解如下幾個點。
奇怪的是,在Scala中任何標記爲implicit的定義都必須放在object或trait中,而不是放在頂層。在上一小節的例子中,咱們將全部的type class instances打包放在JsonWriterInstances中。一樣咱們也能夠把它放在JsonWriter的伴生對象中,這種方式在Scala中有特殊的含義,由於這些instances會直接在implicit scope裏面,無需單獨導入。
正如咱們看到的同樣,編譯器會自動尋找對應類型的type class instances,舉個例子,下面這個例子就會編譯器就會自動尋找JsonWriter[String]對應的instance:
Json.toJson("A string!")
編譯器會從如下幾個implicit scope中尋找適合的instance:
只有用 implicit 關鍵詞標註的instance纔會在 implicit scope,並且若是編譯器在引入的 implicit scope 中發現重複的 instance 聲明,則會編譯抱錯:
implicit val writer1: JsonWriter[String] = JsonWriterInstances.stringWriter implicit val writer2: JsonWriter[String] = JsonWriterInstances.stringWriter Json.toJson("A string") // <console>:23: error: ambiguous implicit values: // both value stringWriter in object JsonWriterInstances of type => JsonWriter[String] // and value writer1 of type => JsonWriter[String] // match expected type JsonWriter[String] // Json.toJson("A string") //
但 Scala 中的 implicit 規則遠比這複雜的多,但這些不在本書的討論範圍以內(若是你想對 implicit 有更深刻的瞭解,能夠參考這些內容:this Stack Overflow post on implicit scope和this blog post on implicit priority)。對於咱們來講,一般把type class instances放在如下四個地方:
若是是第一種方式的,咱們在使用以前經過import導入,第二種方式的話經過繼承trait引入,另外兩種方式的,無需單獨導入,它們默認就在對應類型的implicit scope中。
編譯器除了能直接尋找對應類型type class instance,還擁有組合type class instance的能力。
以前咱們都是經過 implicit val來聲明type class instances ,這很是簡單,實際上咱們有兩種方式去聲明instances:
咱們爲何要經過其餘類型的type class instances來生成新的instances呢?一個很明顯的例子,咱們如何讓Option類型能夠應用JsonWriter這個type class。對於系統中的任意類型的Option[A],都得須要有對應的type class instance,咱們可能會嘗試經過聲明全部instance:
implicit val optionIntWriter: JsonWriter[Option[Int]] = ??? implicit val optionPersonWriter: JsonWriter[Option[Person]] = ??? // and so on...
顯然,這種方式是不易擴展的,對於系統中的任意類型A,咱們都必須去聲明兩個instance,一個做用於A,一個做用於Option[A]。
幸運的是,咱們能夠基於A的instance來構造Option[A]的instance,並且這是一個通用邏輯:
咱們經過implicit def來實現:
implicit def optionWriter[A](implicit writer: JsonWriter[A]): JsonWriter[Option[A]] = new JsonWriter[Option[A]] { def write(option: Option[A]): Json = option match { case Some(aValue) => writer.write(aValue) case None => JsNull } }
這個方法包含一個implicit參數writer,並經過它來構造一個Option[A]的JsonWriter instance。咱們來看一個表達式:
Json.toJson(Option("A string"))
編譯器首先會去尋找對應的type class instance,這裏是optionWriter[String],因此爲表達式加上對應的implicit參數:
Json.toJson(Option("A string"))(optionWriter[String])
由於這裏optionWriter是用implicit def聲明的,並且須要一個implicit writer: JsonWriter[A]參數,因此編譯器會繼續尋找,這裏的對應instance是stringWriter,最終完整的表達式:
Json.toJson(Option("A string"))(optionWriter(stringWriter))
經過這種方式,編譯器會在引入的implicit scope中竟可能的尋找符合的instance,最終組合成所須要類型的type class instance。
Implicit Conversions
在咱們使用implicit def構建type class instance的時候,咱們使用implicit參數,若是咱們不使用implicit聲明參數,編譯器則不會自動去尋找填充參數。
使用implicit方法可是不使用implicit parameters在Scala中是另外一種模式,叫作implicit conversion。跟以前內容中提到的Interface Syntax也是不一樣的,它是一個implicit class並使用擴展方法。implicit conversion是一種古老的編程模式,目前Scala已經不同意使用了。並且當你使用該語法時,編譯器會提出警告,若是你肯定要使用,則需手動引入scala.language.implicitConversions:
implicit def optionWriter[A] (writer: JsonWriter[A]): JsonWriter[Option[A]] = ??? // <console>:18: warning: implicit conversion method optionWriter should be enabled // by making the implicit value scala.language.implicitConversions visible. // This can be achieved by adding the import clause 'import scala.language.implicitConversions' // or by setting the compiler option -language: implicitConversions. // See the Scaladoc for value scala.language.implicitConversions for a discussion // why the feature should be explicitly enabled. // // implicit def optionWriter[A] ^ // error: No warnings can be incurred under -Xfatal-warnings.
Scala能夠經過toString方法將一個任意一個值轉換成String。可是這種方式有一些缺陷:
讓咱們聲明一個Printable type class去解決這些問題吧:
建立一個名爲Printable的object,包含兩個泛型方法:
代碼見示例
咱們能夠把Printable這個功能封裝成類庫,而後在使用的地方引入,咱們先來定義一個case class:
final case class Cat(name: String, age: Int, color: String)
接下來咱們實現一個Printable[Cat]類型的instance,對應format的返回結果應爲:
NAME is a AGE year-old COLOR cat.
代碼見示例
咱們將使用前面介紹的Interface Syntax的語法,讓Printable相關的功能更容易使用:
在PrintableOps[A]聲明兩個方法:
代碼見示例