hangscer

Advanced-Scala之cats学习笔记(一)

2017/06/02

Underscore.io开源了他们的著作,比如《advanced scala with cats》、《essential Slick》、《essential lift》、《essential play》等等大作。借此机会学习学习cats库,开开眼界😂。

至于cats是什么呢?它是一款scala函数式编程库😈。

Type Class

Type classes are a programming pattern originating in Haskell.类型类是源自于Haskell语言的编程模式。They allow us to extends existing libraries with new functionality,without using traditional inheritance,and without altering the original library source code.它允许我们在不去修改源代码的基础上来拓展原有库的功能。

Anatomy of a Type Class

There are three important components to the type class pattern:

  1. the type class itself
  2. instances for particular types
  3. the interface methods that we expose to users

类型类模式有三部分组成:类型类本身、具体类型的实例、留给用户的方法。

The 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.在Cats里,类型类是用至少有一个类型参数(注意:是类型参数)的trait实现。For example,we can represent generic “serialize to JSON” behaviour as follows.举个例子,我们可以编写一个简单的json库以及序列化到json的行为(“serialize to JSON” behaviour):

1
2
3
4
5
//Define a very simple JSON AST 抽象语法树
sealed trait Json
case class JsObject(get:Map[String,Json]) extends Json
case class JsString(get:String) extends Json
case class JsNumber(get:Double) extends Json
1
2
3
4
5
//"serialize to JSON" behaviour
//这里才是类型类
trait JsonWriter[A]{
def write(value:A):Json
}

Type Class Instances

The instances of a type class provide implementations for the types we care about,including types from the Scala standard library and types from our domain model.类型类的具体实例需要提供具体方法,包括来自Scala标准库和自定义的类型。
In Scala we define instances by creating concrete implementations of the type class and tagging them with implicit keyword.在Scala中,我们一般把类型类的具体类型实例贴上隐式implicit关键字。
注:隐式对象实例处必须写明类型

1
2
3
4
5
6
7
8
9
10
11
12
13
case class Person(name:String,email:String)
//这里定义了type class instances
object JsonWriterInstances{
implicit val stringJsonWriter:JsonWriter[String]=new JsonWriter[String] {
override def write(value: String) = JsString(value)
}
implicit val personJsonWriter:JsonWriter[Person]=new JsonWriter[Person] {
override def write(value: Person) = JsObject(Map(
"name"->JsString(value.name),
"email"->JsString(value.email)
))
}
}

interfaces

An interface is any functionality we expose to user.Interfaces to type classes are generic methods that accept instances of the type class as implicit parameters.”接口”是把类型类的实例作为隐式参数的泛型方法。
There are two common ways of specifying an interface:

  1. Interface Objects
  2. Interface Sytax

有两种方法声明接口:接口对象与隐式类转换

Interface Objects

这种写法运用了high-kinded类型

1
2
3
object Json{
def toJson[A](value:A)(implicit w:JsonWriter[A]):Json=w.write(value)
}
1
2
import JsonWriterInstances.personJsonWriter
Json.toJson(Person("hang","hangscer@gmail.com"))

Interface JsonSyntax

我们也可以用隐式转换来达到目的(从类型A->Json的转换):

1
2
3
4
5
object JsonSyntax{
implicit class JsonWriterOps[A](value:A){
def toJson(implicit w:JsonWriter[A]):Json=w.write(value)
}
}
1
2
3
4
import JsonWriterInstances._
import JsonSyntax.JsonWriterOps
val r1=Person("hang","email").toJson
val r2="haha".toJson

由此可以看出,在不修改源代码基础上,给原有类型添加功能实现起来很容易。

Exerise: Printable Library

  1. Define a type class Printable[A] containing a single method format. format should accept a value of type A and returns a String.
  2. Create an object PrintableInstances containing instances of Printable for String and Int.
  3. Define an object Printable with two generic interface methods

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//定义了一个type class
trait Printable[A]{
def format(value:A):String
}
//隐式定义具体实例
object PrintableInstances{
implicit val intPrintable:Printable[Int]=new Printable[Int] {
override def format(value: Int) = s"this is a number:{$value}"
}
implicit val stringPrintable:Printable[String]=new Printable[String] {
override def format(value: String) = s"this is a string:{$value}"
}
}
//对外接口
object Printable{
implicit class PrintableOps[A](x:A){
def format(implicit p:Printable[A]):String=p.format(x)
}
def format[A](x:A)(implicit p:Printable[A])=p.format(x)
}
object Main extends App {
import PrintableInstances.{intPrintable,stringPrintable}
import Printable._
println(1.format)
println(format("asdsa"))
}

Take Home Points

In this section we revisited the concept of a type class,which allows us to add new functionality to existing types.简单说,给已有类型追加方法。
The Scala implementations of a type class has three parts:

  1. the type class itself,a generic trait
  2. instances for each type we care about
  3. one or more generic interface methods

    接下来,我们将会使用Cats来组织类型类等等,还有这种操作。

    Meets Cats

    In this section we will look at how type classes are implmented in Cats.
    Cats is written using a modular structure that allows us to choose which type classes,instances, and interface methods we want to use.Let’s take a first look using cats.Show as an example.Cats将类型类、具体类型实例以及接口模块化。让我们来看看cats.Show这个模块。
    It provides a mechanism for producing developer-friendly console output without using toString.Show提供了更符合开发友好的控制台输出机制。

type class of Show

1
2
3
@typeclass trait Show[T] {
def show(f: T): String
}

Importing Type Classes

Showcats包里。

1
2
3
4
5
import cats.Show
val showInt=Show.apply[Int]
val r:String=showInt.show(1212312)
[error] Main.scala: could not find implicit value for parameter instance: cats.Show[Int]
[error] val showInt=Show.apply[Int]

报错原因:有了类型类,有了interface method,但是导入没有具体类型实例(这就是传说中的二缺一吗?)。

Importing Default Instances

cats.instances包里提供为大部分类型提供的默认类型类实例。

1
2
3
import cats.instances.int.catsStdShowForInt
val showInt=Show.apply[Int]
println(showInt.show(1213))

不妨让我们来看看catsStdShowForInt实例的具体实现:

1
2
3
trait IntInstances extends cats.kernel.instances.IntInstances {
implicit val catsStdShowForInt: Show[Int] = Show.fromToString[Int]
}

Show.fromToString[A]定义在object Show中(interface method定义在object Show中):

1
2
3
4
5
6
7
8
9
object Show {
def show[A](f: A => String): Show[A] = new Show[A] {
def show(a: A): String = f(a)
}
def fromToString[A]: Show[A] = new Show[A] {
def show(a: A): String = a.toString
}
//......
}

Importing Interface Syntax

我们可以从cats.syntax.show包中导入必要的隐式类。额外增加的show方法要求在上下文中存在Show[A]的实例。

1
2
3
import cats.syntax.show._
import cats.instances.int.catsStdShowForInt
println(1.show)

解析一下,让我们来看看三要素:
实例:

1
2
//实例 catsStdShowForInt
implicit val catsStdShowForInt: Show[Int] = Show.fromToString[Int]

interface syntax:

1
implicit def toShowOps[T](target : T)(implicit tc : Show[T]) : Ops[T] = { /* compiled code */ }
1
2
3
4
5
trait Ops[T] extends AnyRef {
val typeClassInstance : Show[T]
def self : T
def show : String = { /* compiled code */ }
}

隐式方法toShowOps[T](target:T)实现了T->Ops[T]隐式类型转换,Ops[T]类具有show等方法。

Defining Custom Instances

Show伴生对象中提供两种方法借此为类型自定义实例。

1
2
3
import cats.syntax.show._ //interface syntax 要素之一
implicit val intShow:Show[Int]=Show.show(i=>s"this is a Number :{$i}")//隐式类型类实例
println(1.show)

Take Home Points

Cats type classes are defined in the cats package. For example, the Show type class is defined as cats.Show.
Default instances are defined in the cats.instances package. Imports are organized by parameter type (as opposed to by type class).
Interface syntax is defined in the cats.syntax package. There are separate syntax imports for each type class. For example, the syntax for Show is defined in cats.syntax.show.

Example:Eq

we will finish off this chapter by looking at another userful type class:cats.Eq.

Equality、Liberty and Fraternity

We can use Eq to define type-safe equality between instances of any given type:
注意:Eq既不是协变,也不是逆变。也就是说,无法与子类型或者父类型比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trait Eq[@sp A] extends Any with Serializable { self =>
def eqv(x: A, y: A): Boolean
def neqv(x: A, y: A): Boolean = !eqv(x, y)
def on[@sp B](f: B => A): Eq[B] =
new Eq[B] {
def eqv(x: B, y: B): Boolean = self.eqv(f(x), f(y))
}
def and(that: Eq[A]): Eq[A] =
new Eq[A] {
def eqv(x: A, y: A) = self.eqv(x, y) && that.eqv(x, y)
}
def or(that: Eq[A]): Eq[A] =
new Eq[A] {
def eqv(x: A, y: A) = self.eqv(x, y) || that.eqv(x, y)
}
}

Comparing Ints

一个小例子,让我们来比较两个Int值:

1
2
3
4
import cats.Eq
import cats.instances.int._
val qeInt=Eq[Int]
println(qeInt.eqv(1,11))

我们也可以导入cats.syntax.eq._来使用===或者=!=方法:

1
2
3
4
import cats.instances.int._
import cats.syntax.eq._
println(1===1)
println(1=!=2)

对于String类型的比较:

1
2
3
import cats.instances.string._
import cats.syntax.eq._
println("11"==="222")

让我们来看看interface method:

1
2
3
4
5
6
7
8
9
trait EqSyntax {
implicit def catsSyntaxEq[A: Eq](a: A): EqOps[A] =
new EqOps[A](a)
}
final class EqOps[A: Eq](lhs: A) {
def ===(rhs: A): Boolean = macro Ops.binop[A, Boolean]
def =!=(rhs: A): Boolean = macro Ops.binop[A, Boolean]
}

catsSyntaxEqA=>EqOps[A]变换。而EqOps具有====!=方法。

Comparing Custom Types

现在让我们来为java.util.Date类定义Eq类型类的实例吧!

1
2
3
4
5
6
7
8
import cats.syntax.eq._
import cats.instances.long._
implicit val dateEqual=Eq.instance[Date]{(date1,date2)=>
date1.getTime===date2.getTime
}
val x=new Date()
val y=new Date()
println(x===y)