文档章节

Scala with Cats 学习笔记1-Type Class 的思考

xmeng
 xmeng
发布于 2019/06/22 16:06
字数 2024
阅读 38
收藏 0

行业解决方案、产品招募中!想赚钱就来传!>>>

Abstract: Scala 中有一个新的概念就是 implicit ,在阅读 Scala with Cats 这本书的第一章着重在介绍这个概念。本文记录在学习过程中个人的一些理解,从而能够帮助更好的在实际项目中应用。

<!--more-->

应用场景

首先我们介绍一下这个 implicitType Class 的一个最典型应用场景:对象序列化

在编程中 ObjectJSON 之间的转化是一项基本的需求,在 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 模式三部曲:

  1. Type Class Definition
  2. Type Class Instance
  3. 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>

xmeng
粉丝 1
博文 5
码字总数 9057
作品 0
南京
私信 提问
加载中
请先登录后再评论。
我的架构演化笔记 功能1: 基本的用户注册

“咚咚”,一阵急促的敲门声, 我从睡梦中惊醒,我靠,这才几点,谁这么早, 开门一看,原来我的小表弟放暑假了,来南京玩,顺便说跟我后面学习一个网站是怎么做出来的。 于是有了下面的一段...

强子哥哥
2014/05/31
976
3
【opencv】图形的绘制

1.矩形图像的绘制: 原函数:void cvRectangle(CvArr* img, CvPoint pt1, CvPoint pt2, CvScalar color, int thickness=1, int line_type=8,int shift=0) img就是需要绘制的图像 pt1 and pt......

其实我是兔子
2014/10/08
1.1K
1
Nutch学习笔记4-Nutch 1.7 的 索引篇 ElasticSearch

上一篇讲解了爬取和分析的流程,很重要的收获就是: 解析过程中,会根据页面的ContentType获得一系列的注册解析器, 依次调用每个解析器,当其中一个解析成功后就返回,否则继续执行下一个解...

强子哥哥
2014/06/26
712
0
桌面即时贴软件--GloboNote

GloboNote 是一个桌面记事软件,可帮你创建待办事宜、提醒和其他笔记信息。无限制即时贴的数量,可分组整理,支持搜索,可定制文本的显示格式(字体、颜色和大小),可将某个即时贴始终显示在...

匿名
2013/01/21
6.6K
1
REST/HTTP 工具包--Spray

Spray 是一个开源的 REST/HTTP 工具包和底层网络 IO 包,基于 Scala 和 Akka 构建。轻量级、异步、非堵塞、基于 actor 模式、模块化和可测试是 spray 的特点。 示例代码: val responses: F...

匿名
2013/02/20
7K
0

没有更多内容

加载失败,请刷新页面

加载更多

springboot 上传文件

package com.taven.demo;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframewor......

tavenpy
10分钟前
0
0
GitHub 标星 44k!史上最全技术面试手册!

整理:迷妹 大家好,我是为前端娱乐圈操碎了心的小迷妹,每天推荐一个小工具/源码,装满你的收藏夹,每天分享一个小技巧,让你轻松节省开发效率,实现不加班不熬夜不掉头发,是我的目标。 每...

祈澈菇凉
今天
0
0
Sublime Text3配置及控制台乱码[cmd杀死进程乱码/编译文件乱码]解决方法

Sublime Text3配置及控制台乱码[cmd杀死进程乱码/编译文件乱码]解决方法 参考文章: (1)Sublime Text3配置及控制台乱码[cmd杀死进程乱码/编译文件乱码]解决方法 (2)https://www.cnblogs...

osc_wl6d9wri
11分钟前
0
0
JS实现网页轮播(每隔5秒切换)

function onload() { <%=sb_js_onload.ToString() %> var timer = setInterval("jump_url()", 1000); } var arrurl = ["screen.aspx?code......

_Somuns
11分钟前
0
0
程序猿:论学习方式的重要性

大家都知道,做我们开发这行的,最核心的竞争力就是学习能力。技术一直在变化,框架一直在更新,学还是不学。 不学,你会落伍,学,太累了,根本学不过来。学习只要找对了方法,也没那么累。...

osc_zg8wy3xa
13分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部