Scala的類型系統(tǒng):取代復(fù)雜的通配符
原創(chuàng)51CTO編輯推薦:Scala編程語言專題
【51CTO獨(dú)家特稿】上次我們講了Scala的類型系統(tǒng),講到了它的可擴(kuò)展性,它的Duck Typing類型推理功能,展示了在類型系統(tǒng)上它比Java更加的靈活。本文中,Martin Odersky將繼續(xù)講解Scala的類型系統(tǒng)。今天的內(nèi)容是映射Java通配符的Existential類型,類型的可變性,以及抽象類的功能。
Existential類型
Bill Venners:最近Scala中添加了一些Existential(存在)類型。我聽說添加Existential類型的理由是為了可以映射所有Java類型到Scala類型,特別是Java的通配符類型。Existential類型數(shù)量是否多于Java通配符類型?它們是否是Java通配符類型的一個(gè)擴(kuò)展集?是否存在其他人們應(yīng)該了解它的理由?
Martin Odersky:這很難說,因?yàn)槿藗儾]有一個(gè)真正的關(guān)于什么是通配符概念。最初由Atsushi Igarashi和Mirko Viroli設(shè)計(jì)的通配符,其靈感來源于Existential類型。事實(shí)上,最初的論文中存在一個(gè)使用Existential類型的編碼。但后來當(dāng)實(shí)際最終設(shè)計(jì)在Java中實(shí)現(xiàn)時(shí),這種聯(lián)系就減少了一些。所以,現(xiàn)在我們真的不了解這些通配符類型的狀況。
Existential類型已經(jīng)出現(xiàn)許多年了,至今為止大約有20年左右。這種類型可以很簡(jiǎn)單地表達(dá)信息。例如你有一個(gè)類型,也許是list(列表)類型,其中一個(gè)列表項(xiàng)的類型你不知道,你只知道它是一些特殊元素類型的列表,但你不知道元素的類型。在Scala中,這就可以表示成一個(gè)Existential類型。語法是List[T] forSome { type T }.。這看起來有點(diǎn)繁瑣。這種繁瑣的語法實(shí)際上是故意的,因?yàn)樗a(chǎn)生的Existential類型通常有點(diǎn)難以處理。現(xiàn)在,Scala有了更好的選擇。它并不需要這么多Existential類型,因?yàn)槲覀兛梢允褂冒渌愋统蓡T的類型。
Scala需要Existential類型有3個(gè)本質(zhì)上的理由。首先,我們需要弄清一些Java通配符的意思,Existential類型就是我們所理解的意思。其次,我們需要弄清一些Java raw(原始)類型的意思,因?yàn)樗鼈內(nèi)蕴幱陬悗?kù)中,是ungenerified(非屬性的)類型。如果你使用一個(gè)Java原始類型,如java.util.List,這是一個(gè)列表,你不知道列表元素的類型。在Scala中這可以被表示成一個(gè)Existential類型。最后,我們需要使用Existential類型來解釋在虛擬機(jī)上發(fā)生著什么事情。Scala像Java一樣,使用泛型擦除模式,所以當(dāng)程序運(yùn)行時(shí),我們不再能看到類型參數(shù)。為了能與Java互用,我們需要進(jìn)行擦除操作。但是,當(dāng)我們做映射或想要表示時(shí),在虛擬機(jī)上會(huì)發(fā)生什么事情?我們需要能夠表達(dá)虛擬機(jī)在使用Scala中的類型時(shí)做了什么事情,Existential類型讓我們做到了這一點(diǎn)。Existential類型可以讓你在不了解類型中某些方面的情況下使用它們。
Bill Venners:您能舉一個(gè)具體的例子嗎?
Martin Odersky:以Scala lists(列表)為例。我希望能夠描述方法的返回類型,head,它會(huì)返回列表第一個(gè)元素(頭一個(gè))。在VM水平,這是一個(gè)List[T] forSome { type T }。我們不知道T是什么。Existential類型理論告訴我們,這是一個(gè)適合某個(gè)類型T的T。這相當(dāng)于根類型——對(duì)象。因此,我們從head方法得到這個(gè)類型。因此在Scala中,當(dāng)我們知道某個(gè)類型時(shí),我們可以消除這些Existential限制。當(dāng)我們不知道某個(gè)類型時(shí),我們就可以使用Existential,Existential類型理論就是在這里給予我們幫助。
Bill Venners:如果您沒有必要擔(dān)心與Java通配符、原始類型和擦除的兼容性,還會(huì)添加Existential類型嗎?如果Java擁有具體化的類型,沒有原始類型和通配符,那么Scala還會(huì)有Existential類型嗎?
Martin Odersky:如果Java擁有具體化的類型,沒有原始類型和通配符,我認(rèn)為Existential類型的使用量就沒那么大了,那么我會(huì)考慮Scala不使用它。
可變性
Bill Venners:在Scala中,是在定義類的時(shí)候定義可變性(variance),而在Java中,是在使用通配符的地方定義它。您能否談?wù)勥@一差異?
Martin Odersky:由于我們可以在Scala中使用Existential類型建模通配符,實(shí)際上如果你想,你也可以在Java中做同樣的事情。但是,我們不鼓勵(lì)你這么做,而是建議使用定義地點(diǎn)可變性(definition site variance)來代替。這是為什么?首先,什么是定義地點(diǎn)可變性?當(dāng)你定義一個(gè)帶有一個(gè)類型參數(shù)的類時(shí),例如List[T],這就帶來了一個(gè)問題。如果你有一個(gè)蘋果列表,那么它同樣也是一個(gè)水果列表嗎?你會(huì)說,當(dāng)然是的。如果蘋果是水果的一個(gè)子類型,那么List[Apple]應(yīng)該是List[Fruit]的一個(gè)子類型。這種子類型關(guān)系被稱為協(xié)變(covariance)。但在某些情況下,這種關(guān)系并不有效。如果我有一個(gè)變量,變量可以保存一個(gè)蘋果,可以保存蘋果類型的一個(gè)引用。但,這不是水果類型的一個(gè)引用,因?yàn)槲也荒芊峙淙魏纹渌o這個(gè)變量,它只能是一個(gè)蘋果。所以,你可以看到,有些情況下我們應(yīng)該有子類型,而有些情況下,子類型就不應(yīng)該有。
Scala的解決方案是注釋類型參數(shù)。如果List在T上是協(xié)變的,我們就可以寫成List[+T]。這將意味著Lists在T上是協(xié)變的。當(dāng)然這存在一些附屬條件。例如,只有當(dāng)沒有人改變List的情況下,我們才可以這么做,否則我們將遇到使用引用時(shí)遇到的同樣問題。
在Scala中會(huì)發(fā)生什么事,這是程序員說的,我認(rèn)為L(zhǎng)ists應(yīng)該是協(xié)變的,這意味著尊重子類型關(guān)系。然后,程序員將會(huì)在聲明的地方,用一個(gè)加號(hào)修飾類型參數(shù)T,針對(duì)所有使用的List只修飾一次。然后編譯器將去找出是否List內(nèi)的所有定義都與其一致。如果存在某些與協(xié)變不符的地方,Scala編譯器將會(huì)提示錯(cuò)誤。Scala擁有一系列的技術(shù)來處理這些錯(cuò)誤,一個(gè)有能力的Scala程序員將會(huì)很快注意到這些錯(cuò)誤,并應(yīng)用這些技術(shù),最終生成一個(gè)錯(cuò)誤處理類。使用者就不必再去顧慮這些錯(cuò)誤了。他們只需知道如果我有一個(gè)List,我就可以在任何地方協(xié)變地使用它。因此,這意味著只有一個(gè)人在寫list類,只有他需要考慮有點(diǎn)難度的問題,但這也不至于太糟糕,因?yàn)榫幾g器會(huì)用錯(cuò)誤提示幫助他。
相比之下,Java帶有通配符的方法意味著在類中你什么都做不了。你只是寫List﹤T>。然后,如果用戶想要一個(gè)協(xié)變list,他們不寫List﹤Fruit>,而是寫List﹤? extends Fruit>。所以這是一個(gè)通配符。問題是,這是用戶代碼。這些用戶通常都沒有類庫(kù)設(shè)計(jì)人員那么專業(yè)。此外,這些注釋間一個(gè)單一的不匹配將會(huì)帶來類型錯(cuò)誤。因此,難怪你會(huì)得到大量與通配符有關(guān)的非常棘手的錯(cuò)誤信息,我認(rèn)為這是Java泛型最重要的罪魁禍?zhǔn)?。因?yàn)檫@種通配符的方法對(duì)于普通人來說確實(shí)是太復(fù)雜、太難于處理。
可變性是當(dāng)你結(jié)合泛型和子類型時(shí)非常重要的東西,但它也很復(fù)雜。沒有辦法能完全讓它變成一件小事。我們做的比Java好的地方是,可以讓你只在類庫(kù)中做一次,使得用戶不需要考慮和處理它。
抽象類
Bill Venners:在Scala中,一個(gè)類型可以是另一個(gè)類型的成員,就如同方法和域可以是一個(gè)類型的成員。在Scala中,這些類型成員可以是抽象的,就如同在Java中方法可以抽象。在抽象類型成員和泛型參數(shù)之間是否存在重疊?為什么Scala兩者都包含?抽象類型具有哪些泛型所不具有的功能?
Martin Odersky:抽象類型確實(shí)具有一些泛型所不具有的功能,但首先讓我陳述一個(gè)稍微普遍的原理。一直都存在兩個(gè)抽象概念:參數(shù)和抽象成員。在Java中,兩者都有,但它取決于你在抽象什么。在Java中你可以有抽象方法,但你不能把方法作為參數(shù)傳遞。你并不擁有抽象域,但可以傳值作為參數(shù)。同樣,你沒有抽象類型成員,但你可以指定一種類型作為參數(shù)。因此,在Java中你可以有以上3種方式,但使用什么抽象原則是有區(qū)別的。你可以爭(zhēng)辯說,這種區(qū)別是相當(dāng)武斷的。
我們?cè)赟cala中所做的是力求更全面和垂直。我們決定對(duì)以上所有3種成員都采用同樣的構(gòu)造原則。所以,你可以有抽象域,也可以有值參數(shù)。你可以傳遞方法(或“函數(shù)”)作為參數(shù),或者也可以抽象它們。您可以指定類型作為參數(shù),或者也可以抽象它們。我們概念性地得到的是,我們可以按照其它的建模另一個(gè)。至少在原則上,我們可以表達(dá)各種參數(shù)為一種面向?qū)ο蟮某橄蟆R虼?,在某種意義上可以說Scala是一種更垂直、更全面的語言。
現(xiàn)在,問題仍然存在,這能給你帶來什么好處?抽象類型是對(duì)以上我們談到的問題的很好的處理,一個(gè)已經(jīng)存在了很長(zhǎng)一段時(shí)間的標(biāo)準(zhǔn)問題是動(dòng)物和食物。讓人不解的是,有個(gè)動(dòng)物類,帶有一個(gè)吃一些食物的方法。問題是,如果我們建立一個(gè)動(dòng)物類的子類,如牛,那么它們將只吃草,而不是任意食物。例如,牛不會(huì)吃魚。你真正想要的是一個(gè)牛類,帶有一個(gè)只吃草而不吃其它東西的方法。實(shí)際上,在Java中你不能這樣做,如像前面提到的分配一個(gè)任意的水果給蘋果變量的問題。
問題是,你怎么辦?答案是,你為動(dòng)物類添加一個(gè)抽象類型。你說,新的動(dòng)物類中含有一個(gè)SuitableFood(適當(dāng)食物)的類型,這我不知道。因此這是一個(gè)抽象類型。你并不給出類型實(shí)現(xiàn)。然后,你就可以有一個(gè)吃的方法,只吃適當(dāng)?shù)氖澄?。然后在牛類中,我?huì)說,好吧,我有一個(gè)牛類,它繼承于動(dòng)物類,并且對(duì)于牛類型來說,適當(dāng)?shù)氖澄锞褪遣?。因此,在子類中就可以?shí)現(xiàn)這些抽象。
現(xiàn)在,你可以說,我可以用參數(shù)完成同樣的事情。事實(shí)上,你確實(shí)可以。你可以給動(dòng)物類添加參數(shù),參數(shù)為各種所吃的食物。但在實(shí)踐中,當(dāng)你要完成很多事情的時(shí)候,這就導(dǎo)致了參數(shù)爆炸,而且通常更重要的是,參數(shù)的范圍。在1998年的ECOOP ,Kim Bruce, Phil Wadler和我一起發(fā)過一個(gè)文章,我們指出,隨著你增加你所不知道的東西的數(shù)量,典型的程序?qū)?huì)以2次方程式的數(shù)量增加。因此,我們有理由盡量不用參數(shù),而是使用抽象成員。
適應(yīng)新的語法
Bill Venners:當(dāng)人們隨機(jī)查看Scala代碼時(shí),我認(rèn)為有兩件事可以使它看上去有點(diǎn)神秘。一個(gè)是DSL是他們不熟悉的,就像是解析器或XML類庫(kù)。另一個(gè)是類型系統(tǒng)的各種各樣的表達(dá)式,特別是表達(dá)式的聯(lián)合。Scala程序員如何能掌握這樣的語法?
Martin Odersky:當(dāng)然這里存在很多新東西,必須進(jìn)行學(xué)習(xí)和吸收。因此,這將花費(fèi)一些時(shí)間。我相信我們需要繼續(xù)努力研究的一件事是更好的工具支持。現(xiàn)在,當(dāng)你獲得類型錯(cuò)誤時(shí),我們?cè)噲D給你一個(gè)不錯(cuò)的錯(cuò)誤信息。有時(shí)候,錯(cuò)誤信息有很多行,能夠解釋得更好。我們盡力做好,但我認(rèn)為如果我們能有更好的交互性,我們將可以做的更好。
試想一下,如果有一個(gè)動(dòng)態(tài)類型語言,對(duì)于一個(gè)錯(cuò)誤信息,只有3到4行的錯(cuò)誤提示??赡懿粫?huì)有調(diào)試器,不會(huì)有堆棧跟蹤,只有3到4行提示信息,如“空指針廢棄,”也許會(huì)有發(fā)生錯(cuò)誤的行號(hào)。在這種情況下,我不認(rèn)為動(dòng)態(tài)語言會(huì)是非常受歡迎的。當(dāng)然,這不是真實(shí)發(fā)生的事情。實(shí)際上,你擁有一個(gè)調(diào)試器,可以讓你快速找到錯(cuò)誤根源。
對(duì)于類型,我們還沒有這些設(shè)施。我們所有的只是一些錯(cuò)誤信息。如果你有一個(gè)非常豐富和富有表現(xiàn)力的類型系統(tǒng),它需要更多的知識(shí)來理解這些錯(cuò)誤信息,你想要更多幫助。因此,在未來我們要研究的一件事是,我們是否能夠真正給你一個(gè)更具有互動(dòng)性的環(huán)境,例如,如果類型出現(xiàn)問題,你可以找出錯(cuò)誤原因。例如,如何讓編譯器指出這個(gè)表達(dá)式的類型應(yīng)該是這個(gè),以及它為什么不認(rèn)為這個(gè)類型符合其它預(yù)期類型。你可以交互式探索這些東西。如果這樣,我想,由于類型所導(dǎo)致的錯(cuò)誤將能夠更容易被發(fā)現(xiàn)。
另一方面,一些語法很新,需要一段時(shí)間的適應(yīng)。這也許是我們無法避免的。我們只希望在今后兩三年內(nèi),人們將能夠完全熟悉這些類型。其他的一些主流語言在剛推出時(shí)也遇到過類似問題。我非常清楚地記得,當(dāng)異常捕獲語句剛出現(xiàn)時(shí),人們就覺得它很奇怪,花了很長(zhǎng)時(shí)間來適應(yīng)。當(dāng)然,現(xiàn)在每個(gè)人都認(rèn)為這是很自然的。Scala也面臨這樣的問題,尤其是在類型方面,還需要人們漸漸去適應(yīng)。
【相關(guān)閱讀】