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