"There are two ways of constructing a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies." -- C.A.R. Hoare
「型變(Variance)」是一個(gè)令人費(fèi)解的概念,但它卻是理解類型系統(tǒng)的重要基石。本文首先討論型變的基本概念,深入理解型變的基本形態(tài)。然后以List, Option為例講解型變?cè)?code>Scala中的應(yīng)用;最后通過ScalaHamcrest的實(shí)戰(zhàn),加深對(duì)此概念的理解和運(yùn)用。
1. 定義
1.1 術(shù)語表
| 英語 | 中文 | 示例 |
|---|---|---|
| Variance | 型變 | Function[-T, +R] |
| Nonvariant | 不變 | Array[A] |
| Covariant | 協(xié)變 | Supplier[+A] |
| Contravariant | 逆變 | Consumer[-A] |
| Immutable | 不可變的 | String |
| Mutable | 可變的 | StringBuilder |
其中,Mutable常常意味著Nonvariant,但是Noncovariant與Mutable分別表示兩個(gè)不同的范疇。
1.2 ****形式化****
「型變(Variance)」擁有三種基本形態(tài):協(xié)變(Covariant), 逆變(Contravariant), 不變(Nonconviant),可以形式化地描述為:
一般地,假設(shè)類型
C[T]持有類型參數(shù)T;給定兩個(gè)類型A和B,如果滿足A <: B,則C[A]與C[B]之間存在三種關(guān)系:
- 如果
C[A] <: C[B],那么C是協(xié)變的(Covariant);- 如果
C[A] :> C[B],那么C是逆變的(Contravariant);- 否則,
C是不變的(Nonvariant)。
1.3 Scala****表示****
Scala的類型參數(shù)使用+標(biāo)識(shí)「協(xié)變」,-標(biāo)識(shí)「逆變」,而不帶任何標(biāo)識(shí)的表示「不變」(Nonvariable)。
trait C[+A] // C is covariant
trait C[-A] // C is contravariant
trait C[A] // C is nonvariant
2. 準(zhǔn)則
事實(shí)上,判定一個(gè)類型是否擁有型變能力的準(zhǔn)則非常簡單。
一般地,「不可變的」(Immutable)類型意味著「型變」(Variant),而「可變的」(Mutable)意味著「不變」(Nonvariant)。
其中,對(duì)于不可變的(Immutable)類型
C[T]
- 如果它是一個(gè)生產(chǎn)者,其類型參數(shù)應(yīng)該是協(xié)變的,即
C[+T];- 如果它是一個(gè)消費(fèi)者,其類型參數(shù)應(yīng)該是逆變的,即
C[-T]。
2.1 生產(chǎn)者
Supplier是一個(gè)生成者,它生產(chǎn)T類型的實(shí)例。
trait Supplier[+T] {
def get: T
}
2.2 消費(fèi)者
Consumer是一個(gè)消費(fèi)者,它消費(fèi)T類型的實(shí)例。
trait Consumer[-T] {
def accept(t: T): Unit
}
2.3 函數(shù)
Function1是一個(gè)一元函數(shù),它既是一個(gè)生產(chǎn)者,又是一個(gè)消費(fèi)者,但它是不可變的(Immutable)。其中,入?yún)㈩愋蜑?code>-T,返回值類型為+R;對(duì)于參數(shù)類型,函數(shù)是逆變的,而對(duì)于返回值類型,函數(shù)則是協(xié)變的。
trait Function1[-T, +R] {
def apply(t: T): R
}
2.4 數(shù)組
與Function1不同,雖然數(shù)組類型既是一個(gè)生產(chǎn)者,又是一個(gè)消費(fèi)者。但是,它是一個(gè)可變的(Mutable)類型,因此它是不變的(Nonvariant)。
final class Array[T](val length: Int) {
def apply(i: Int): T = ???
def update(i: Int, x: T): Unit = ???
}
綜上述,可以得到2個(gè)簡單的結(jié)論。
2.5 結(jié)論1
對(duì)于不可變的(
Immutable)類型:C[-T, +R, S],
- 逆變(Contravariant)的類型參數(shù)
T只可能作為函數(shù)的參數(shù);- 協(xié)變(Covariant)的類型參數(shù)
R只可能作為函數(shù)的返回值;- 不變的(Nonvariable)類型參數(shù)
S則沒有限制,即可以作為函數(shù)的參數(shù),也可以作為返回值。
幸運(yùn)的是,Scala編譯器能夠完成這個(gè)約束的檢查。例如,
trait Array[+A] {
def update(a: A): Unit
}
編譯器將檢測(cè)到編譯錯(cuò)誤。
error: covariant type A occurs in contravariant position in type A of value a
def update(a: A): Unit
^
2.6 結(jié)論2
如果
T2 <: T1,且R1 <: R2,那么(T1 => R1) <: (T2 => R2)。
例如,給定兩個(gè)函數(shù)F1, F2。
type F1 = Option[Int] => Some[Int]
type F2 = Some[Int] => Option[Int]
則F1 <: F2成立。
3. 函數(shù)式的數(shù)據(jù)結(jié)構(gòu)
3.1 自制Option
Option是一個(gè)遞歸的數(shù)據(jù)結(jié)構(gòu),它要么是Some,要么是None。其中,None表示為空,是遞歸結(jié)束的標(biāo)識(shí)。

使用Scala,可以很直觀地完成Option的遞歸定義。
sealed trait Option[+A]
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]
因?yàn)?code>Option是不可變的(Immutable),因此Option應(yīng)該設(shè)計(jì)為協(xié)變的,即Option[+A]。也就是說,對(duì)于任意的類型A,Option[Nothing] <: Option[A],即None <: Option[A]都成立。
3.2 自制List
與Option類似,List也是一個(gè)遞歸的數(shù)據(jù)結(jié)構(gòu),它由頭部和尾部組成。其中,Nil表示為空,是遞歸結(jié)束的標(biāo)識(shí)。

使用Scala,可以很直觀地完成List的遞歸定義。
sealed trait List[+A]
case class Cons[A](head: A, tail: List[A]) extends List[A]
case object Nil extends List[Nothing]
因?yàn)?code>List是不可變的(Immutable),因此List應(yīng)該設(shè)計(jì)為協(xié)變的,即List[+A]。也就是說,對(duì)于任意的類型A,List[Nothing] <: List[A],即Nil <: List[A]都成立。
3.2.1 實(shí)現(xiàn)cons
可以在List中定義了cons算子,用于在List頭部追求元素。
sealed trait List[+A] {
def cons(a: A): List[A] = Cons(a, this)
}
此時(shí),編譯器將報(bào)告協(xié)變類型A出現(xiàn)在逆變的位置上的錯(cuò)誤。因此,在遵循「里氏替換」的基本原則,使用「下界(Lower Bound)」對(duì)A進(jìn)行界定,轉(zhuǎn)變?yōu)椤覆蛔兊?Nonvariable)」的類型參數(shù)A1。
sealed trait List[+A] {
def cons[A1 :> A](a: A1): List[A1] = Cons(a, this)
}
至此,又可以得到一個(gè)重要的結(jié)論。
3.2.2 結(jié)論3
對(duì)于不可變的(Immutable)類型:
C[-T, +R],
- 當(dāng)協(xié)變類型參數(shù)
R出現(xiàn)在函數(shù)參數(shù)時(shí),使用「下界」R1 >: R進(jìn)行界定,將其轉(zhuǎn)變?yōu)椴蛔兊?Nonvariable)類型參數(shù)R1;- 當(dāng)逆變類型參數(shù)
T出現(xiàn)在函數(shù)返回值時(shí),使用「上界」T1 <: T進(jìn)行界定,將其轉(zhuǎn)變?yōu)椴蛔兊?Nonvariable)類型參數(shù)T1。
List的cons算子就是通過使用「下界」界定協(xié)變類型參數(shù)A,將其轉(zhuǎn)變?yōu)椴蛔兊?Nonvariable)類型參數(shù)A1的。而對(duì)于「上界」,通過實(shí)現(xiàn)ScalaHamcrest的基本功能進(jìn)行講述,并完成整個(gè)型變理論知識(shí)的回顧和應(yīng)用。
4. 實(shí)戰(zhàn)ScalaHamcrest
對(duì)于任意的類型A,A => Boolean常常稱為「謂詞」;如果該謂詞用于匹配類型A的某個(gè)值,也常常稱該謂詞為「匹配器」。
ScalaHamcrest首先定義一個(gè)Matcher,并添加了&&, ||, !的基本操作,用于模擬謂詞的基本功能。
class Matcher[A](pred: A => Boolean) extends (A => Boolean) {
self =>
def &&(that: Matcher[A]): Matcher[A] =
new Matcher[A](x => self(x) && that(x))
def ||(that: Matcher[A]): Matcher[A] =
new Matcher[A](x => self(x) || that(x))
def unary_! : Matcher[A] =
new Matcher[A](x => !self(x))
def apply(x: A): Boolean = pred(x)
}
4.1 支持型變
對(duì)于函數(shù)A => Boolean,類型參數(shù)A是逆變的。因此,為了得到支持型變能力的Matcher,應(yīng)該將類型參數(shù)A聲明為逆變。
class Matcher[-A](pred: A => Boolean) extends (A => Boolean) {
self =>
// error: contravariant type A occurs in covariant position.
def &&(that: Matcher[A]): Matcher[A] =
new Matcher[A](x => self(x) && that(x))
// error: contravariant type A occurs in covariant position.
def ||(that: Matcher[A]): Matcher[A] =
new Matcher[A](x => self(x) || that(x))
def unary_! : Matcher[A] =
new Matcher[A](x => !self(x))
def apply(x: A): Boolean = pred(x)
}
但是,此時(shí)&&, ||將報(bào)告逆變類型A出現(xiàn)在協(xié)變的位置上。為此,可以使用「上界」對(duì)A進(jìn)行界定,轉(zhuǎn)變?yōu)椴蛔兊?Nonvariant)類型A1。
對(duì)于逆變的類型
Matcher[-A],當(dāng)它作為函數(shù)函數(shù)參數(shù)時(shí),其型變能力將置反。因此,定義def &&(that: Matcher[A]): Matcher[A]時(shí),that的類型實(shí)際為Matcher[+A]。
class Matcher[-A](pred: A => Boolean) extends (A => Boolean) {
self =>
def &&[A1 <: A](that: Matcher[A1]): Matcher[A1] =
new Matcher[A1](x => self(x) && that(x))
def ||[A1 <: A](that: Matcher[A1]): Matcher[A1] =
new Matcher[A1](x => self(x) || that(x))
def unary_![A1 <: A]: Matcher[A1] =
new Matcher[A1](x => !self(x))
def apply(x: A): Boolean = pred(x)
}
4.2 原子匹配器
基于Matcher,可以定義特定的原子匹配器。例如:
case object Always extends Matcher[Any](_ => true)
case object Never extends Matcher[Any](_ => false)
也可以定義EqualTo的原子匹配器,用于比較對(duì)象間的相等性。
class EqualTo[-A](expected: A) extends Matcher[A] (
_ == expected
)
object EqualTo {
def apply[A](expected: A) = new EqualTo(expected)
}
與EqualTo類似,可以定義原子匹配器Same,用于比較對(duì)象間的一致性。
class Same[-A <: AnyRef](expected: A) extends Matcher[A] (
expected eq _
)
object Same {
def apply[A <: AnyRef](expected: A) = new Same(expected)
}
其中,A <: AnyRef類型對(duì)A進(jìn)行界定,排除AnyVal的子類誤操作Same。類似于類型上界,也可以使用其他的類型界定形式;例如,可以定義InstanceOf,對(duì)類型A進(jìn)行上下文界定,用于匹配某個(gè)實(shí)例的類型。
class InstanceOf[-T : ClassTag] extends Matcher[Any] (
_ match {
case _: T => true
case _ => false
}
)
object InstanceOf {
def apply[T : ClassTag] = new InstanceOf[T]
}
有時(shí)候,基于既有的原子可以很方便地構(gòu)造出新的原子。
case object IsNil extends EqualTo[AnyRef](null)
case object Empty extends EqualTo("")
4.3 組合匹配器
也可以將各個(gè)原子或者組合器進(jìn)行組裝,形成威力更為強(qiáng)大的組合器。
case class AllOf[-A](matchers: Matcher[A]*) extends Matcher[A] (
actual => matchers.forall { _(actual) }
)
case class AnyOf[-A](matchers: Matcher[A]*) extends Matcher[A] (
actual => matchers.exists { _(actual) }
)
特殊地,基于AnyOf/AllOf,可以構(gòu)造很多特定的匹配器。
object Blank extends Matcher[String] (
"""\s*""".r.pattern.matcher(_).matches
)
object EmptyOrNil extends AnyOf(IsNil, Empty)
object BlankOrNil extends AnyOf(IsNil, Blank)
4.4 修飾匹配器
修飾也是一種特殊的組合行為,用于完成既有功能的增強(qiáng)和補(bǔ)充。
case class Not[-A](matcher: Matcher[A]) extends Matcher[A] (
!matcher(_)
)
case class Is[-A](matcher: Matcher[A]) extends Matcher[A] (
matcher(_)
)
其中,Not, Is是兩個(gè)普遍的修飾器,可以修飾任意的匹配器;也可以定義針對(duì)特定類型的修飾器。例如,可以定義針對(duì)字符串操作的原子匹配器和修飾匹配器。
case class Starts(prefix: String) extends Matcher[String] (
_ startsWith prefix
)
case class Ends(suffix: String) extends Matcher[String] (
_ endsWith suffix
)
case class Contains(substr: String) extends Matcher[String] (
_ contains substr
)
如果要忽略大小寫,則可以通過定義IgnoringCase,修飾既有的字符串的原子匹配器。
case class IgnoringCase(matcher: Matcher[String]) extends Matcher[String] (
s => matcher(s.toLowerCase)
)
object IgnoringCase {
def equalTo(str: String) = IgnoringCase(EqualTo(str.toLowerCase))
def starts(str: String) = IgnoringCase(Starts(str.toLowerCase))
def ends(str: String) = IgnoringCase(Ends(str.toLowerCase))
def contains(str: String) = IgnoringCase(Contains(str.toLowerCase))
}
4.5 語法糖
有時(shí)候,可以通過定義語法糖,提升用戶感受。例如,可以使用Not替換Not(EqualTo),Is替代Is(EqualTo),不僅減輕用戶的負(fù)擔(dān),而且還能提高表達(dá)力。
object Not {
def apply[A](expected: A): Not[A] = Not(EqualTo(expected))
}
object Is {
def apply[A](expected: A): Is[A] = Is(EqualTo(expected))
}
4.6 測(cè)試用例
至此,還不知道ScalaHamcrest如何使用呢?可以定義一個(gè)實(shí)用方法assertThat。
def assertThat[A](actual: A, matcher: Matcher[A]) {
assert(matcher(actual))
}
其中,assert定義于Predef之中。例如存在如下一個(gè)測(cè)試用例。
assertThat(2, AllOf(Always, InstanceOf[Int], Is(2), EqualTo(2)))
也可以使用&&直接連接多個(gè)匹配器形成調(diào)用鏈,替代AllOf匹配器。
assertThat(2, Always && InstanceOf[Int] && Is(2) && EqualTo(2))
5. 未來演進(jìn)
此處為了演示「型變」的作用,ScalaHamcrest采用了OO與FP相結(jié)合的設(shè)計(jì)手法,在下一章講解「Scala函數(shù)論」時(shí),ScalaHamcrest將采用純函數(shù)式的設(shè)計(jì)手法實(shí)現(xiàn),敬請(qǐng)關(guān)注。