Scala型變

"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芥喇,但是NoncovariantMutable分別表示兩個(gè)不同的范疇。

1.2 ****形式化****

「型變(Variance)」擁有三種基本形態(tài):協(xié)變(Covariant), 逆變(Contravariant), 不變(Nonconviant)凰萨,可以形式化地描述為:

一般地继控,假設(shè)類型C[T]持有類型參數(shù)T;給定兩個(gè)類型AB胖眷,如果滿足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]

  1. 逆變(Contravariant)的類型參數(shù)T只可能作為函數(shù)的參數(shù)驻呐;
  2. 協(xié)變(Covariant)的類型參數(shù)R只可能作為函數(shù)的返回值灌诅;
  3. 不變的(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í)。

Option: Is the Bucket Empty or Full?

使用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ì)于任意的類型AOption[Nothing] <: Option[A]脆霎,即None <: Option[A]都成立总处。

3.2 自制List

Option類似,List也是一個(gè)遞歸的數(shù)據(jù)結(jié)構(gòu)睛蛛,它由頭部和尾部組成鹦马。其中,Nil表示為空忆肾,是遞歸結(jié)束的標(biāo)識(shí)荸频。

List的遞歸結(jié)構(gòu)

使用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ì)于任意的類型AList[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]刹帕,

  1. 當(dāng)協(xié)變類型參數(shù)R出現(xiàn)在函數(shù)參數(shù)時(shí),使用「下界」R1 >: R進(jìn)行界定谎替,將其轉(zhuǎn)變?yōu)椴蛔兊?Nonvariable)類型參數(shù)R1偷溺;
  2. 當(dāng)逆變類型參數(shù)T出現(xiàn)在函數(shù)返回值時(shí),使用「上界」T1 <: T進(jìn)行界定钱贯,將其轉(zhuǎn)變?yōu)椴蛔兊?Nonvariable)類型參數(shù)T1挫掏。

Listcons算子就是通過(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采用了OOFP相結(jié)合的設(shè)計(jì)手法飘诗,在下一章講解「Scala函數(shù)論」時(shí)与倡,ScalaHamcrest將采用純函數(shù)式的設(shè)計(jì)手法實(shí)現(xiàn),敬請(qǐng)關(guān)注昆稿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纺座,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子溉潭,更是在濱河造成了極大的恐慌净响,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喳瓣,死亡現(xiàn)場(chǎng)離奇詭異馋贤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)畏陕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門配乓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人惠毁,你說(shuō)我怎么就攤上這事扰付。” “怎么了仁讨?”我有些...
    開(kāi)封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵羽莺,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我洞豁,道長(zhǎng)盐固,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任丈挟,我火速辦了婚禮刁卜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘曙咽。我一直安慰自己蛔趴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布例朱。 她就那樣靜靜地躺著孝情,像睡著了一般。 火紅的嫁衣襯著肌膚如雪洒嗤。 梳的紋絲不亂的頭發(fā)上箫荡,一...
    開(kāi)封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音渔隶,去河邊找鬼羔挡。 笑死洁奈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绞灼。 我是一名探鬼主播利术,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼低矮!你這毒婦竟也來(lái)了印叁?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤商佛,失蹤者是張志新(化名)和其女友劉穎喉钢,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體良姆,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肠虽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了玛追。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片税课。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖痊剖,靈堂內(nèi)的尸體忽然破棺而出韩玩,到底是詐尸還是另有隱情,我是刑警寧澤陆馁,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布找颓,位于F島的核電站,受9級(jí)特大地震影響叮贩,放射性物質(zhì)發(fā)生泄漏击狮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一益老、第九天 我趴在偏房一處隱蔽的房頂上張望彪蓬。 院中可真熱鬧,春花似錦捺萌、人聲如沸档冬。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)酷誓。三九已至,卻和暖如春慈参,著一層夾襖步出監(jiān)牢的瞬間呛牲,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工驮配, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓壮锻,卻偏偏與公主長(zhǎng)得像琐旁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猜绣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容