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. 使用扩展方法对上一个例子进行不同实现;

代码见示例

相关文章
相关标签/搜索