Abstract: Scala 中有一个新的概念就是 implicit
,在阅读 Scala with Cats 这本书的第一章着重在介绍这个概念。本文记录在学习过程中个人的一些理解,从而能够帮助更好的在实际项目中应用。
<!--more-->
应用场景
首先我们介绍一下这个 implicit
和 Type Class
的一个最典型应用场景:对象序列化
在编程中 Object
和 JSON
之间的转化是一项基本的需求,在 Java
中最常用的是使用 Jackson
通过注解在类上定义其序列化的要求,比如字段名称等等,然后在应用时通过注解自动的实现了对象和 JSON
的转化。
在 Scala
中,我们通向希望能够达到这个目标,提前定义好序列化的方法,在需要的时候自动调用。
下面介绍一下我们的需求:
定义一个类,可以直接对实例对象转化为 JSON,比如 Person 对象,可以通过 person.toJson
或者是 XXX.toJson(person)
对象转 JSON
实现
序列化的过程无非是把不同类型的字段转化为字符串的过程。
1. 创建抽象语法树
// Define a very simple 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
2. 创建 Type Class
A type class is an interface or API that represents some functionality we want to implement. In Cats a type class is represented by a trait with at least one type parameter.
// The "serialize to JSON" behaviour is encoded in this trait
trait JsonWriter[A] {
def write(value: A): Json
}
这个包含有一个方法的接口被称为 Type Class
,为什么叫 type
,参考这篇文章scala类型系统:26) type classes模式
这个模式被称为type classes,源于haskell,我对haskell不熟悉,为什么用这么奇怪的一个名字?从这儿看到:在haskell里没有类似于java/scala里的class的概念,所以class这个术语可以随意使用,它的意思相当于”class of types”,可以理解为对某一系列类型的抽象(即高阶类型)。
scala里的type classes模式主要通过隐式参数来实现,但需要注意的是,并不是所有的隐式参数都可以理解为type classes模式,隐式参数的类型必须是泛型的(高阶),它表示某种抽象行为,这种行为的具体实现要由它的具体类型参数决定。
3. 实现 Type Class
的接口
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...
}
我们可以看到,我们定义 trait
的时候,类型是一个泛型,这里实现了两种类型的 write
方法,一种是基础的 String
,一种是我们业务的 Person
类。
这里我们使用 implicit
,说明这个方法并不会直接被调用。
4. 使用 Type Class
的接口
这里有两种方法,
使用静态方法 Interface Objects
object Json {
def toJson[A](value: A)(implicit w: JsonWriter[A]): Json =
w.write(value)
}
使用接口文法 Interface Syntax
object JsonSyntax {
implicit class JsonWriterOps[A](value: A) {
def toJson(implicit w: JsonWriter[A]): Json =
w.write(value)
}
}
5. 应用
Interface Objects
import JsonWriterInstances._
Json.toJson(Person("Dave", "dave@example.com"))
// 等价于
//Json.toJson(Person("Dave", "dave@example.com"))(personWriter)
Interface Syntax
import JsonWriterInstances._
import JsonSyntax._
Person("Dave", "dave@example.com").toJson
原理
当我们最后调用 Json.toJson
或者是 .toJson
的时候,因为其定义带有 implicit w: JsonWriter[A]
这个隐含参数,所以 Scala 就会尝试去寻找对应匹配类型的 JsonWriter
,如果是 String
就会找到 stringWriter
, 如果是 Person
就会找到 personWriter
。然后在使用这个函数传入到 Json.toJson
或者是 .toJson
方法中,最后调用其 write
方法实现转换。
总的来说,就是其他定义 implicit
方法,然后传入参数指定其为 implicit
,最后 Scala
自动寻找匹配类型的隐含方法并应用。
Cats Show 做了什么?
我们阅读源码 org.typelevel/cats-core_2.12/srcs/cats-core_2.12-1.4.0-sources.jar!/cats/Show.scala
/**
* A type class to provide textual representation. It is meant to be a
* better "toString". Whereas toString exists for any Object,
* regardless of whether or not the creator of the class explicitly
* made a toString method, a Show instance will only exist if someone
* explicitly provided one.
*/
trait Show[T] extends Show.ContravariantShow[T]
trait ContravariantShow[-T] extends Serializable {
def show(t: T): String
}
这里的 Show
和上面的 JsonWriter
同样的角色,定义了 show
方法。
对于 Show
的实现并不在这个文件中,但是 Cats 包含了一个 package object instances
这里面有很多基础类型的 instance
, 比如:
trait StringInstances extends cats.kernel.instances.StringInstances {
implicit val catsStdShowForString: Show[String] =
Show.fromToString[String]
}
接口定义:
trait Ops[A] {
def typeClassInstance: Show[A]
def self: A
def show: String = typeClassInstance.show(self)
}
trait ToShowOps {
implicit def toShow[A](target: A)(implicit tc: Show[A]): Ops[A] = new Ops[A] {
val self = target
val typeClassInstance = tc
}
}
trait ShowSyntax extends Show.ToShowOps {
implicit final def showInterpolator(sc: StringContext): Show.ShowInterpolator = Show.ShowInterpolator(sc)
}
如何使用呢?只需要定义一个 我们需要类型的 instance,就可以直接使用了。
implicit val catShow = Show.show[Cat] { cat =>
import cats.instances.int._ // for Show
import cats.instances.string._ // for Show
val name = cat.name.show
val age = cat.age.show
val color = cat.color.show
s"$name is a $age year-old $color cat."
}
println(Cat("Garfield", 38, "ginger and black").show)
总结
Scala 的 Type Class
模式三部曲:
- Type Class Definition
- Type Class Instance
- Type Class Interface (Syntax/Objects)
另外这段话说的很好:
简单总结,type classes模式通过泛型来描述某种通用行为,对每个想要用到这种行为的具体类型,在现实时行为部分并不放在类型自身中,而是通过实现一个type class实例(对泛型具化),最后在调用时(通过隐式参数)让编译器自动寻找行为的实现。
我们可以那这 Show
思考一下,传统做法是什么呢? 应该是定义一个统一的接口,接口中有一个 show
的方法,然后哪个类想实现 show
,就实现这个接口。如果出现类型依赖,就要依赖其 show
的实现。(其实这就是 Java Object
类的 toString
)
虽然这样做并没有问题,但是 type class 模式进一步解耦合,类型天生并不绑定的其能力,能力是单独定义 (Type Class),一个类型如果需要这种能力,只需要单独创建其能力的实现(Instance),并由编译器自动绑定。
有时间我们可以阅读以下 Play Framework 关于请求 Json 序列化过程的代码 (或者 Lagom Framework)里面大量使用了这种模式。
Gist
// Define a very simple 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
////////////////// Step 1
// The "serialize to JSON" behaviour is encoded in this trait
trait JsonWriter[A] {
def write(value: A): Json
}
final case class Person(name: String, email: String)
////////////////// Step 2
// any definitions marked implicit in Scala MUST BE placed inside an object or trait
object JsonWriterInstances {
implicit val stringWriter: JsonWriter[String] =
(value: String) => JsString(value)
implicit val personWriter: JsonWriter[Person] =
(value: Person) =>
JsObject(
Map("name" -> JsString(value.name), "email" -> JsString(value.email))
)
// etc...
}
////////////////// Step 3 - 1
object Json {
def toJson[A](value: A)(implicit w: JsonWriter[A]): Json =
w.write(value)
}
////////////////// Step 3 - 2
object JsonSyntax {
implicit class JsonWriterOps[A](value: A) {
def toJson(implicit w: JsonWriter[A]): Json =
w.write(value)
}
}
////////////////// Usage
object Main extends App {
import JsonWriterInstances._
Json.toJson(Person("Dave", "dave@example.com"))
import JsonWriterInstances._
import JsonSyntax._
Person("Dave", "dave@example.com").toJson
}
////////////////// Cats Show Usage
final case class Cat(name: String, age: Int, color: String)
object CatShowMain extends App {
implicit val catShow = Show.show[Cat] { cat =>
import cats.instances.int._ // for Show
import cats.instances.string._ // for Show
val name = cat.name.show
val age = cat.age.show
val color = cat.color.show
s"nameisaage year-old $color cat."
}
println(Cat("Garfield", 38, "ginger and black").show)
println(Cat("Garfield", 38, "ginger and black").show)
}
////////////////// Cats Show Source code
// 1
trait Show[T] extends Show.ContravariantShow[T]
trait ContravariantShow[-T] extends Serializable {
def show(t: T): String
}
// 2 (default, and need to define the customized class type instance)
trait StringInstances extends cats.kernel.instances.StringInstances {
implicit val catsStdShowForString: Show[String] =
Show.fromToString[String]
}
// 3
trait Ops[A] {
def typeClassInstance: Show[A]
def self: A
def show: String = typeClassInstance.show(self)
}
trait ToShowOps {
implicit def toShow[A](target: A)(implicit tc: Show[A]): Ops[A] = new Ops[A] {
val self = target
val typeClassInstance = tc
}
}
trait ShowSyntax extends Show.ToShowOps {
implicit final def showInterpolator(sc: StringContext): Show.ShowInterpolator = Show.ShowInterpolator(sc)
}
<script src='https://gitee.com/xmeng1/codes/sfi1lbqjnraomyp2wtu7416/widget_preview?title=TypeClassDemo.scala'></script>
<script src="https://gist.github.com/xmeng1/851522ba5b83450ed00114c3d58a60ae.js"></script>
相关推荐
赠送jar包:scala-parser-combinators_2.11-1.0.4.jar; 赠送原API文档:scala-parser-combinators_2.11-1.0.4-javadoc.jar; 赠送源代码:scala-parser-combinators_2.11-1.0.4-sources.jar; 赠送Maven依赖信息...
它是一款scala函数式编程库,是Underscore.io开源的著作。
scala-SDK-4.7.0-vfinal-2.12-linux.gtk.x86_64.tar.gz scala-SDK-4.7.0-vfinal-2.12-linux.gtk.x86_64.tar.gz
赠送jar包:scala-parser-combinators_2.12-1.1.0.jar; 赠送原API文档:scala-parser-combinators_2.12-1.1.0-javadoc.jar; 赠送源代码:scala-parser-combinators_2.12-1.1.0-sources.jar; 赠送Maven依赖信息...
1. **Scala编译器**:这是将Scala源代码转换为Java字节码的工具。Scala编译器能够处理函数式编程和面向对象编程的语法,使得开发者可以在一个统一的环境中工作。 2. **Scala库**:这个库提供了大量的内置模块和类,...
赠送jar包:scala-xml_2.12-1.0.6.jar; 赠送原API文档:scala-xml_2.12-1.0.6-javadoc.jar; 赠送源代码:scala-xml_2.12-1.0.6-sources.jar; 赠送Maven依赖信息文件:scala-xml_2.12-1.0.6.pom; 包含翻译后的API...
《Scala with Cats》是一本专注于Scala语言的函数式编程的书籍,旨在帮助读者掌握Cats库的使用,这是Scala中一个非常流行的函数式编程库。通过阅读和实践这本书,读者可以深入理解类型类、隐式、函子、单子等函数式...
programming-scala-1st-edition-code-examples programming-scala-1st-edition-code-examples programming-scala-1st-edition-code-examples programming-scala-1st-edition-code-examples
在 Scala 中与 MongoDB 进行交互,通常我们会使用 `mongo-scala-driver`,而不是 `mongo-java-driver`,因为 Scala 驱动提供了更符合 Scala 语言特性的 API 设计。本示例将详细介绍如何使用 `mongo-scala-driver` ...
"scala-intellij-bin-2016.3.9"是针对Scala语言的一个特定版本的IntelliJ IDEA插件,该版本为2016.3.9。这个插件是专门为Scala开发者设计的,旨在提高他们在IntelliJ IDEA中的开发体验。 Scala是一种多范式编程语言...
IDEA 全称 IntelliJ IDEA,是java编程语言开发的集成环境。IntelliJ在业界被公认为最好的java开发工具,尤其在智能代码助手、代码自动提示、重构、JavaEE支持、各类版本工具(git、...通过该插件可支持Scala语言的构建。
"scala-intellij-bin-2021.1.22.zip" 是一个包含IntelliJ IDEA的Scala插件的压缩包,版本号为2021.1.22,适用于IntelliJ IDEA的2021.1版本。 Scala插件是IntelliJ IDEA为了提升Scala开发体验而设计的,它提供了丰富...
在本压缩包"scala-intellij-bin-2016.3.1.zip"中,包含的是IntelliJ IDEA的一个版本,专门针对Scala语言进行了优化。 IntelliJ IDEA是JetBrains公司开发的,它以其智能代码补全、强大的代码分析和重构工具而闻名。...
赠送jar包:scala-parser-combinators_2.11-1.0.4.jar; 赠送原API文档:scala-parser-combinators_2.11-1.0.4-javadoc.jar; 赠送源代码:scala-parser-combinators_2.11-1.0.4-sources.jar; 包含翻译后的API...
scala-SDK-3.0.1-vfinal-2.10-win32.win32.x86.part01.rar
"scala-intellij-bin-2019.1.2.zip"这个压缩包就是针对IntelliJ IDEA的Scala插件,版本为2019.1.2,用于增强IDE对Scala语言的支持。 首先,我们需要理解Scala插件的作用。在IntelliJ IDEA中安装此插件后,用户可以...
`scala-intellij-bin-2018.3.5.zip` 和 `scala-intellij-bin-2018.3.6.zip` 是两个版本的Scala插件,分别适用于IntelliJ IDEA的2018.3.5和2018.3.6版本。这些插件是为了增强IDE对Scala语言的支持,提供代码高亮、...
赠送jar包:scala-xml_2.11-1.0.4.jar; 赠送原API文档:scala-xml_2.11-1.0.4-javadoc.jar; 赠送源代码:scala-xml_2.11-1.0.4-sources.jar; 赠送Maven依赖信息文件:scala-xml_2.11-1.0.4.pom; 包含翻译后的API...
赠送jar包:scala-xml_2.11-1.0.1.jar; 赠送原API文档:scala-xml_2.11-1.0.1-javadoc.jar; 赠送源代码:scala-xml_2.11-1.0.1-sources.jar; 赠送Maven依赖信息文件:scala-xml_2.11-1.0.1.pom; 包含翻译后的API...
赠送jar包:scala-compiler-2.11.8.jar; 赠送原API文档:scala-compiler-2.11.8-javadoc.jar; 赠送源代码:scala-compiler-2.11.8-sources.jar; 赠送Maven依赖信息文件:scala-compiler-2.11.8.pom; 包含翻译后...