"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)用芯义;最后通過(guò)ScalaHamcrest
的實(shí)戰(zhàn)剂桥,加深對(duì)此概念的理解和運(yùn)用凯旭。
1. 定義
1.1 術(shù)語(yǔ)表
英語(yǔ) | 中文 | 示例 |
---|---|---|
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)則非常簡(jiǎ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ǎn)單的結(jié)論搀玖。
2.5 結(jié)論1
對(duì)于不可變的(
Immutable
)類型:C[-T, +R, S]
,
- 逆變(Contravariant)的類型參數(shù)
T
只可能作為函數(shù)的參數(shù)驻呐;- 協(xié)變(Covariant)的類型參數(shù)
R
只可能作為函數(shù)的返回值灌诅;- 不變的(Nonvariable)類型參數(shù)
S
則沒(méi)有限制,即可以作為函數(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]
钥弯。也就是說(shuō)径荔,對(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]
场仲。也就是說(shuō)和悦,對(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
算子就是通過(guò)使用「下界」界定協(xié)變類型參數(shù)A
,將其轉(zhuǎn)變?yōu)椴蛔兊?Nonvariable)類型參數(shù)A1
的秩命。而對(duì)于「上界」尉共,通過(guò)實(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
)
如果要忽略大小寫喻括,則可以通過(guò)定義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 語(yǔ)法糖
有時(shí)候唬血,可以通過(guò)定義語(yǔ)法糖,提升用戶感受唤崭。例如拷恨,可以使用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. 未來(lái)演進(jìn)
此處為了演示「型變」的作用,ScalaHamcrest
采用了OO
與FP
相結(jié)合的設(shè)計(jì)手法飘诗,在下一章講解「Scala函數(shù)論」時(shí)与倡,ScalaHamcrest
將采用純函數(shù)式的設(shè)計(jì)手法實(shí)現(xiàn),敬請(qǐng)關(guān)注昆稿。