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型变的主要内容,如果未能解决你的问题,请参考以下文章