Scala-with-cats中文翻譯(一):Type class與Implicit

前言

最近在學習Cats,發現Scala-with-cats這本書寫的不錯,因此有想法將其翻譯成中文,另外也能夠在翻譯的過程當中加深理解,另外我會對每部份內容建議須要瞭解的程度,幫助你們更好的學習總體內容(有部份內容理解起來比較晦澀且不經常使用,瞭解便可),同時我也將相關的練習代碼放到github上了,你們可下載參考:scala-with-cats,有翻譯不許確的地方,也但願你們能指正🙏。git

本篇內容主要爲Type class與Implicit,這應該算是學習Cats須要瞭解的最基礎的內容。es6

學習程度:須要徹底掌握

1.1 剖析Type class

Type class模式主要由3個模塊組成:github

  • Type class 自己
  • Type class Instances
  • Type class interface

1.1.1 Type Class

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

1.1.2 Type Class Instances

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...
}

1.1.3 Type Class Interfaces

Type Class Interface 包含對咱們想要對外部暴露的功能。interfaces是指接受 type class instance 做爲 implicit 參數的泛型方法。scala

一般有兩種方式去建立 interface:翻譯

  • Interface Objects
  • Interface Syntax
Interface Objects用法

建立 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 Syntax用法

咱們也可使用擴展方法使已存在的類型擁有 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)
使用implicitly

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

1.2 使用 Implicits

對於 Scala 來講,使用 type class 就得跟 implicit values 和 implicit parameters 打交道,爲了更好的使用它,咱們須要瞭解如下幾個點。

1.2.1 組織 Implicits

奇怪的是,在Scala中任何標記爲implicit的定義都必須放在object或trait中,而不是放在頂層。在上一小節的例子中,咱們將全部的type class instances打包放在JsonWriterInstances中。一樣咱們也能夠把它放在JsonWriter的伴生對象中,這種方式在Scala中有特殊的含義,由於這些instances會直接在implicit scope裏面,無需單獨導入。

1.2.2 Implicit 做用域

正如咱們看到的同樣,編譯器會自動尋找對應類型的type class instances,舉個例子,下面這個例子就會編譯器就會自動尋找JsonWriter[String]對應的instance:

Json.toJson("A string!")

編譯器會從如下幾個implicit scope中尋找適合的instance:

  • 自身及繼承範圍內的 instance
  • 導入範圍內的 instance
  • 對應 type class 以及參數類型的伴生對象中

只有用 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 scopethis blog post on implicit priority)。對於咱們來講,一般把type class instances放在如下四個地方:

  1. 一個單獨的object中,好比上面提到的JsonWriterInstances;
  2. 一個單獨的trait中;
  3. type class的伴生對象中;
  4. 咱們所使用類型的伴生對象中,好比JsonWriter[A],即A的伴生對象中;

若是是第一種方式的,咱們在使用以前經過import導入,第二種方式的話經過繼承trait引入,另外兩種方式的,無需單獨導入,它們默認就在對應類型的implicit scope中。

1.2.3 遞歸尋找Implicit

編譯器除了能直接尋找對應類型type class instance,還擁有組合type class instance的能力。

以前咱們都是經過 implicit val來聲明type class instances ,這很是簡單,實際上咱們有兩種方式去聲明instances:

  1. 經過 implicit val來聲明具體類型的type class instances;
  2. 利用 implicit methods經過其餘類型的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,並且這是一個通用邏輯:

  • 假如option是Some(a: A),則使用A的instance;
  • 假如option是None,則返回JsNull;

咱們經過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.

1.3 練習: 實現一個Printable

Scala能夠經過toString方法將一個任意一個值轉換成String。可是這種方式有一些缺陷:

  • 它對Scala中的每一個類型都進行了實現,可是使用有很大限制;
  • 不能對特定類型進行特定的實現;

讓咱們聲明一個Printable type class去解決這些問題吧:

  1. 聲明一個type class Printable[A]包含一個方法format,該方法接受一個類型爲A的參數並返回String。
  2. 建立一個名爲PrintableInstances的object,包含Printable[String]和Printable[Int]的instance聲明。
  3. 建立一個名爲Printable的object,包含兩個泛型方法:

    1. format方法:接受一個類型爲A的參數和相關類型的Printable,使用Printable將參數轉換爲String。
    2. print方法:與format方法參數一致,但返回值時Unit,它執行的操做是經過println將類型爲A的參數輸出到控制檯。

代碼見示例

1.3.1 使用Printable

咱們能夠把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.

 代碼見示例

1.3.2 更好的 Syntax語法

咱們將使用前面介紹的Interface Syntax的語法,讓Printable相關的功能更容易使用:

  1. 建立一個PrintableSyntax的object。
  2. 在PrintableSyntax中聲明一個implicit class PrintableOps[A]對A類型的值進行包裝。
  3. 在PrintableOps[A]聲明兩個方法:

    • format接受一個implicit Printable[A]的參數,返回String;
    • print接受一個implicit Printable[A]的參數,返回Unit;
  4. 使用擴展方法對上一個例子進行不同實現;

代碼見示例

相關文章
相關標籤/搜索