FPDIS 3.Type-Directed Programming
这一小节都是阅读内容和测验
Motivating Example & Type-Directed Programming
考虑一个排序函数
1 | def sort(xs: List[Int]): List[Int] = { |
这个排序只能实现对Int
类型的排序,如果我们把它推广下
1 | def sort[A](xs: List[A])(lessThan: (A, A) => Boolean): List[A] = { |
把比较函数作为第二个参数传入,就能实现通用的排序方法
其实Scala自带排序相关的类Ordering
,上面的代码还能这样写
1 | def sort[A](xs: List[A])(ord: Ordering[A]): List[A] = { |
但是这样还有个问题,就是每次都需要传入不同的Ordering
1 | import scala.math.Ordering |
为了减少不必要的套版代码,可以使用隐式参数
1 | def sort[A](xs: List[A])(implicit ord: Ordering[A]): List[A] |
关于隐式参数的详细讲解,可以参考 Programming in Scala
习题需要升级才能提交,这里就不放出答案了。(下同)
所谓面向类型编程( type-directed programming) 能够从类型中推导出值来
编译器会在以下范围内搜索缺少的隐式类型
- 类型为 T
- 有implicit关键字
- 函数调用时可见, 或者在和T相关的伴生类中有定义
多个implciit可用时,如果优先级相等,会报ambiguous implicit value
但是内涵更多的implicit 定义优先级更高
- 具体类型大于泛型
- 子类大于父类
Implicit Query
1 | scala> implicitly[Ordering[Int]] |
Type Classes
接着上面Ordering
的例子
1 | trait Ordering[A] { |
这种情况下,我们称Ordering
是一个type class
(类型类)
type class
可以帮助我们实现多态
现在考虑实现一个分数类,并且给它也加上隐式的Ordering
实现
1 | /** A rational number |
对于Ordering
这个隐式类来说,为了能使sort
函数正常工作,它的实现应该满足如下性质
- 自反 compare(x, y) 和 compare(y, x)的符号应该相反
- 传递 如果 compare(x, y) < 0, compare(y, z) < 0 那么 compare(x, z) < 0
- 一致 如果 x, y 相等, 那么 compare(x, z) 与 compare(y, z) 的符号应该相同
这是wikipedia种对环(ring)的定义
In mathematics, a ring is one of the fundamental algebraic structures used in abstract algebra. It consists of a set equipped with two binary operations that generalize the arithmetic operations of addition and multiplication. Through this generalization, theorems from arithmetic are extended to non-numerical objects such as polynomials, series, matrices and functions.
This structure is so common that, by abstracting over the ring structure, developers could write programs that could then be applied to various domains
(arithmetic, polynomials, series, matrices and functions).
环 <R, +, *>满足以下定律
- 是一个加法上的交换群
- 是一个乘法上的Monid
- 满足乘法上的分配律
下面一一解释其中的术语
在离散数学中,群 G, 包含二元运算符·
有这些性质
封闭性
$$
\forall a, b ∈ G, 有a · b \in G
$$
结合律
$$
\forall a, b, c ∈ G, 有 (a · b) · c = a · (b · c)
$$
有单位元
$$
\exists e ∈ G, 使得 \forall a∈G,都有 e · a = a · e = a
$$
有逆元
$$
\forall a \in G, \exists b \in G, 使得 a · b = b · a = e
$$
若一个群满足交换律
$$
\forall a, b \in G, a · b = b · a
$$
则,群有被称为交换群(阿贝尔群/加法群)
Monid需要有如下性质
- 有单位元
- 满足结合律
乘法分配律好理解
- 左分配律 a ⋅ (b + c) = (a · b) + (a · c)
- 右分配律 (b + c) · a = (b · a) + (c · a)
所有满足的性质可以看这张表(来自课程中的内容)
对于上述环,可以这样建模
1 | trait Ring[A] { |
标准库的type class Numeric
就满足环的性质
满足环性质的数据结构,可以方便地进行并行计算
Conditional Implicit Definitions
隐式参数是可以嵌套推导的
1 | implicit def a: A = ... |
当我们用 implicit query 查询 implicitly[D]
时
能链式的查到implicit def a
这样做的意义在后面编程作业的编写过程中,自然会发现。
当我们发现隐式参数可以连续推导时,自然很容易产生这么一个疑问?
可不可以递归地使用隐式定义,这样做有什么意义?
答曰:超纲,不在本课的讨论范围内。
Implicit Conversions
隐式转换(Implicit Conversion)用来自动转换类型,来丰富API
考虑这么一个抽象语法树(Abstract Syntax Tree, AST)
1 | sealed trait Json |
我们可以这样定义个json
1 | JObject("name" -> JString("Paul"), "age" -> JNumber(42)) |
但其实这样并不简洁,最好是能达到 下面这样的结果
1 | def obj(fields: (String, Json)*): Json = JObject(fields: _*) |
有一个办法是在伴生类中定义隐式转换
1 | object Json { |
这样就简单了
1 | scala> import Json._ |
另外一个例子,是对方法做扩展
1 | import java.util.concurrent.TimeUnit |
我们希望做如下的简化
1 | val delay = Duration(15, TimeUnit.SECONDS) |
这样只要引入implicit class
就行
1 | case class Duration(value: Int, unit: TimeUnit) |
14.seconds
会被编译成new HasSeconds(15).seconds
编译器会在以下情况下寻找隐式转换
- T不符合表达式的预期类型
- 在选择e.m中,如果成员m在T上不可访问
- 在选择e.m(args)中,如果成员m在T上可访问,但不适用于参数args
使用隐式转换时要特别谨慎,因为会把一个类型自动转成另一个类型,滥用的话可能导致用户抓狂
调试隐式转换时,scalac的选项-Xprint:typer
可以让我们看到经过编译器转换隐式推断后的代码
隐式转换是一个核武器
,不是什么时候都用它,能用更小的代价优化代码,就不要滥用隐式转换,过多的隐式转换会导致代码繁杂。
编程作业: Json编码解码器
问题背景
本次作业的目标是实现一个Type Directed 的 序列化库
它使用面向类型的操作,组合简单类型值的序列化器,为更复杂类型的值构造序列化器。
Json的数据结构定义如下
1 | sealed trait Json |
举个具体的例子,对于一个Json字符串数据
1 | { |
在代码中的表现为
1 | Json.Obj(Map( |
Encoder
做的工作,就是把不同类型的数据转换成Json class
的类型
其定义签名如下
1 | trait Encoder[-A] { |
这里的-A
不知道还有印象没有,这是代表逆变
的意思
即 如果 Arr
是 Json
类型的子类,那么 Encoder[Arr]
是 Encoder[Json]
的父类
依据里氏替换原则,父类可以被等价地替换为子类
这意味着,程序中所有出现Encoder[Arr]
的地方,都能被替换成Encoder[Json]
更多关于Variances(型变/逆变/不变)的说明可以参考官方文档
与其他具体的 Json 值(Num
, Str
)不同,Json Object
比较特殊,可以将两个Object
组合起来
以构建一个更大的 Json Object
,其中包含两个对象的所有字段。
在名为ObjectEncoder的Encoder子类中zip
定义其实现
1 | /** |
相对地,Decoder[A]将Json类型的值转换为A类型的值:
1 | trait Decoder[+A] { |
这里使用Option[A]
而不是A
作为返回类型,是因为在 Json 数据的解析过程中,可能会出现失败的情况
这里提供了一些工具类,就不赘述了
Our Tasks
我们的任务,是从简单的基本类型开始,构建 Json 解析器的实例,直到能实现对 case class 的解析为止
简单的验证工作可以用sbt console
来完成,但要注意的是修改代码后需要重启REPL才能更新
首先要实现的是
- Encoder[String], Encoder[Boolean]
- Decoder[Int], Decoder[String], Decoder[Boolean]
可以复用Encoder.fromFunction
, Decoder.fromFunction
, Decoder.fromPartialFunction
的逻辑
同时需要确保Docoder[Int]
能拒绝浮点数类型的输入
下面是一个简单的实现
1 | /** An encoder for `String` values */ |
基本类型的编解码函数实现后,需要实现的是集合List
类型的函数,已经给出了Encoder
部分的实现
也可以简单地验证
1 | implicit def listEncoder[A](implicit encoder: Encoder[A]): Encoder[List[A]] = |
照猫画虎,可以实现Decoder
1 | /** |
实现后也可以在REPL中测试
1 | scala> import codecs._; import Util._ |
我们来看ObjectEncoder
的例子
1 | scala> val xField = ObjectEncoder.field[Int]("x") |
对于多个字段,可以这样实现
1 | scala> val pointEncoder: ObjectEncoder[(Int, Int)] = { |
依据ObjectEncoder.field
实现 Decoder.field
就简单了
1 | // ObjectEncoder的实现 |
接下来我们需要实现case class
的编码/解码工作
当前已经有Encoder[Person]
的实现代码了,现在需要实现的是
Decoder[Person]
Encoder[Contacts]
,Decoder[Contacts]
1 | case class Person(name: String, age: Int) |
有了之前实现多个Decoder
的经验,不难想到,我们需要的是使用Decoder.field
提取出每一个Person
中的每一个字段,使用zip
组合,再用transform
转换成需要的类
1 | implicit val personDecoder: Decoder[Person] = { |
写完后可以简单测试下
1 | scala> import codecs._ |
至于Encoder[Contacts]
和Decoder[Contacts]
的实现,用之前的listEncoder
和listDecoder
就行
下面把需要用到的代码放一起方便查看
1 | implicit def listDecoder[A](implicit decoder: Decoder[A]): Decoder[List[A]] = |
下面是一个实现,目前是依靠IDE的提示对类型配平做出来的
为什么这样实现,你问我,我问谁
1 | trait ContactsCodecs { |
附加题
你能显示的写出编译器推断
Encoder[List[Person]]
的结果吗1
2
3
4
5
6
7
8
9Encoder[List[Person]]
= Encoder[listEncoder[Person](implicit encoder: Encoder[Person])]
= Encoder[listEncoder[Person](implicit encoder: Encoder[Person])]
= Encoder[listEncoder[Person](
ObjectEncoder.field[String]("name")(Encoder[String])
.zip(ObjectEncoder.field[Int]("age"))(Encoder[Int])
.transform[Person](user => (user.name, user.age))
)
= 下面就不展开了(要吐了,编译器也太强了可以对
Option
的字段定义编码/解码器吗由下一个问题,知可以
可不可以对类型
A
定义一个隐式实例Encoder[Option[A]]
(就像Encoder[List[A]]
一样)蹩脚地模仿着实现
1
2
3
4
5implicit def optionEncoder[A](implicit encoder: Encoder[A]): Encoder[Option[A]] =
Encoder.fromFunction{
case Some(e) => encoder.encode(e)
case _ => Json.Null
}除了
case class
,能不能对sealed trait
也定义编解器这样问了,肯定是可以,比如https://github.com/circe/circe-derivation/issues/8
我还没弄明白怎么实现的。
版权声明:
除另有声明外,本博客文章均采用 知识共享(Creative Commons) 署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议 进行许可。
分享