最近在学习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]声明两个方法:
代码见示例