共计 7714 个字符,预计需要花费 20 分钟才能阅读完成。
前言
最近在学习 Cats,发现 Scala-with-cats 这本书写的不错,所以有想法将其翻译成中文,另外也可以在翻译的过程中加深理解,另外我会对每部分内容建议需要了解的程度,帮助大家更好的学习整体内容(有部分内容理解起来比较晦涩且不常用,了解即可),同时我也将相关的练习代码放到 github 上了,大家可下载参考:scala-with-cats,有翻译不准确的地方,也希望大家能指正????。
本篇内容主要为 Type class 与 Implicit,这应该算是学习 Cats 需要了解的最基础的内容。
学习程度:需要完全掌握
1.1 剖析 Type class
Type class 模式主要由 3 个模块组成:
- 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 类型相关的代码。
1.1.2 Type Class Instances
Type Class instance 就是特定类型的 Type Class 实现,包括 Scala 的基本类型以及我们自己定义的类型。
在 Scala 中,Type Class instance 可以通过实现对应类型 Type Class 来声明,并用 implicit 这个关键词进行标记:
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
参数的泛型方法。
通常有两种方式去创建 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 scope 和 this blog post on implicit priority)。对于我们来说,通常把 type class instances 放在以下四个地方:
- 一个单独的 object 中,比如上面提到的 JsonWriterInstances;
- 一个单独的 trait 中;
- type class 的伴生对象中;
- 我们所使用类型的伴生对象中,比如 JsonWriter[A],即 A 的伴生对象中;
如果是第一种方式的,我们在使用之前通过 import 导入,第二种方式的话通过继承 trait 引入,另外两种方式的,无需单独导入,它们默认就在对应类型的 implicit scope 中。
1.2.3 递归寻找 Implicit
编译器除了能直接寻找对应类型 type class instance,还拥有组合 type class instance 的能力。
之前我们都是通过 implicit val 来声明 type class instances,这非常简单,实际上我们有两种方式去声明 instances:
- 通过 implicit val 来声明具体类型的 type class instances;
- 利用 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 去解决这些问题吧:
- 声明一个 type class Printable[A]包含一个方法 format,该方法接受一个类型为 A 的参数并返回 String。
- 创建一个名为 PrintableInstances 的 object,包含 Printable[String]和 Printable[Int]的 instance 声明。
-
创建一个名为 Printable 的 object,包含两个泛型方法:
- format 方法:接受一个类型为 A 的参数和相关类型的 Printable,使用 Printable 将参数转换为 String。
- 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 相关的功能更容易使用:
- 创建一个 PrintableSyntax 的 object。
- 在 PrintableSyntax 中声明一个 implicit class PrintableOps[A]对 A 类型的值进行包装。
-
在 PrintableOps[A]声明两个方法:
- format 接受一个 implicit Printable[A]的参数,返回 String;
- print 接受一个 implicit Printable[A]的参数,返回 Unit;
- 使用扩展方法对上一个例子进行不一样实现;
代码见示例