Scala型变

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Scala型变相关的知识,希望对你有一定的参考价值。

参考技术A 「型变(Variance)」是一个令人费解的概念,但它却是理解类型系统的重要基石。本文首先讨论型变的基本概念,深入理解型变的基本形态。然后以 List, Option 为例讲解型变在 Scala 中的应用;最后通过 ScalaHamcrest 的实战,加深对此概念的理解和运用。

其中, Mutable 常常意味着 Nonvariant ,但是 Noncovariant 与 Mutable 分别表示两个不同的范畴。

「型变(Variance)」拥有三种基本形态:协变(Covariant), 逆变(Contravariant), 不变(Nonconviant),可以形式化地描述为:

Scala 的类型参数使用 + 标识「协变」, - 标识「逆变」,而不带任何标识的表示「不变」(Nonvariable)。

事实上,判定一个类型是否拥有型变能力的准则非常简单。

Supplier 是一个生成者,它生产 T 类型的实例。

Consumer 是一个消费者,它消费 T 类型的实例。

Function1 是一个一元函数,它既是一个生产者,又是一个消费者,但它是不可变的(Immutable)。其中,入参类型为 -T ,返回值类型为 +R ;对于参数类型,函数是逆变的,而对于返回值类型,函数则是协变的。

与 Function1 不同,虽然数组类型既是一个生产者,又是一个消费者。但是,它是一个可变的(Mutable)类型,因此它是不变的(Nonvariant)。

综上述,可以得到 2 个简单的结论。

幸运的是, Scala 编译器能够完成这个约束的检查。例如,

编译器将检测到编译错误。

例如,给定两个函数 F1, F2 。

则 F1 <: F2 成立。

Option 是一个递归的数据结构,它要么是 Some ,要么是 None 。其中, None 表示为空,是递归结束的标识。

使用 Scala ,可以很直观地完成 Option 的递归定义。

因为 Option 是不可变的(Immutable),因此 Option 应该设计为协变的,即 Option[+A] 。也就是说,对于任意的类型 A , Option[Nothing] <: Option[A] ,即 None <: Option[A] 都成立。

与 Option 类似, List 也是一个递归的数据结构,它由头部和尾部组成。其中, Nil 表示为空,是递归结束的标识。

使用 Scala ,可以很直观地完成 List 的递归定义。

因为 List 是不可变的(Immutable),因此 List 应该设计为协变的,即 List[+A] 。也就是说,对于任意的类型 A , List[Nothing] <: List[A] ,即 Nil <: List[A] 都成立。

可以在 List 中定义了 cons 算子,用于在 List 头部追求元素。

此时,编译器将报告协变类型 A 出现在逆变的位置上的错误。因此,在遵循「里氏替换」的基本原则,使用「下界(Lower Bound)」对 A 进行界定,转变为「不变的(Nonvariable)」的类型参数 A1 。

至此,又可以得到一个重要的结论。

List 的 cons 算子就是通过使用「下界」界定协变类型参数 A ,将其转变为不变的(Nonvariable)类型参数 A1 的。而对于「上界」,通过实现 ScalaHamcrest 的基本功能进行讲述,并完成整个型变理论知识的回顾和应用。

对于任意的类型 A , A => Boolean 常常称为「谓词」;如果该谓词用于匹配类型 A 的某个值,也常常称该谓词为「匹配器」。

ScalaHamcrest 首先定义一个 Matcher ,并添加了 &&, ||, ! 的基本操作,用于模拟谓词的基本功能。

对于函数 A => Boolean ,类型参数 A 是逆变的。因此,为了得到支持型变能力的 Matcher ,应该将类型参数 A 声明为逆变。

但是,此时 &&, || 将报告逆变类型 A 出现在协变的位置上。为此,可以使用「上界」对 A 进行界定,转变为不变的(Nonvariant)类型 A1 。

基于 Matcher ,可以定义特定的原子匹配器。例如:

也可以定义 EqualTo 的原子匹配器,用于比较对象间的相等性。

与 EqualTo 类似,可以定义原子匹配器 Same ,用于比较对象间的一致性。

其中, A <: AnyRef类型 对 A 进行界定,排除 AnyVal 的子类误操作 Same 。类似于类型上界,也可以使用其他的类型界定形式;例如,可以定义 InstanceOf ,对类型 A 进行上下文界定,用于匹配某个实例的类型。

有时候,基于既有的原子可以很方便地构造出新的原子。

也可以将各个原子或者组合器进行组装,形成威力更为强大的组合器。

特殊地,基于 AnyOf/AllOf ,可以构造很多特定的匹配器。

修饰也是一种特殊的组合行为,用于完成既有功能的增强和补充。

其中, Not, Is 是两个普遍的修饰器,可以修饰任意的匹配器;也可以定义针对特定类型的修饰器。例如,可以定义针对字符串操作的原子匹配器和修饰匹配器。

如果要忽略大小写,则可以通过定义 IgnoringCase ,修饰既有的字符串的原子匹配器。

有时候,可以通过定义语法糖,提升用户感受。例如,可以使用 Not 替换 Not(EqualTo) , Is 替代 Is(EqualTo) ,不仅减轻用户的负担,而且还能提高表达力。

至此,还不知道 ScalaHamcrest 如何使用呢?可以定义一个实用方法 assertThat 。

其中, assert 定义于 Predef 之中。例如存在如下一个测试用例。

也可以使用 && 直接连接多个匹配器形成调用链,替代 AllOf 匹配器。

此处为了演示「型变」的作用, ScalaHamcrest 采用了 OO 与 FP 相结合的设计手法,在下一章讲解「Scala函数论」时, ScalaHamcrest 将采用纯函数式的设计手法实现,敬请关注。

以上是关于Scala型变的主要内容,如果未能解决你的问题,请参考以下文章

Scala Type Parameters 2

Scala系列

Scala语言专题

Scala语言概述

[原创]Scala学习:编写Scala脚本

Scala - 01 - Scala简介