上一篇
目錄
21. 結構型別
結構型別(Strucural Types)經常被描述為「型別安全的鴨子型別(duck typing)」,如果你想獲得一個直觀的理解,這是一個很好的比較。
迄今為止,我們在型別方面考慮的都是這樣的問題:「它實現了介面 X 嗎?」,有了「結構型別」,我們就可以深入一步,開始對一個指定物件的結構(因此得名)進行推理。當我們在檢查一個採用了結構型別的型別匹配問題時,我們需要把問題改為:「這裡存在帶有這種簽名的方法嗎?」。
讓我們舉一個很常見的例子,來看看它為什麼如此強大。想象你有很多支援被 closed 的東西,在 Java 裡,通常會實現 java.io.Closeable
介面,以便寫出一些常用的 Closeable
工具類(事實上,Google Guava 就有這樣的一個類)。現在再想象有人還實現了一個 MyOwnCloseable
類,但沒有實現 java.io.Closeable
。由於靜態型別的緣故,你的 Closeables
類庫就會出問題,你就不能傳 MyOwnCloseable
的例項給它。讓我們使用結構型別來解決這個問題:
type JavaCloseable = java.io.Closeable
// reminder, it's body is: { def close(): Unit }
class MyOwnCloseable {
def close(): Unit = ()
}
// method taking a Structural Type
def closeQuietly(closeable: { def close(): Unit }) =
try {
closeable.close()
} catch {
case ex: Exception => // ignore...
}
// accepts a java.io.File (implements Closeable):
closeQuietly(new StringReader("example"))
// accepts a MyOwnCloseable
closeQuietly(new MyOwnCloseable)複製程式碼
這個結構型別被作為方法的一個引數。基本上可以說,我們對這個型別唯一的期望就是它應該存在內部(close
)這樣一個方法。它可以擁有更多的方法,因此這裡並不是一個完全匹配,而是這個型別必須定義最小的一組方法,這樣才能有效。
另外需要注意的是,使用結構型別對執行時效能存在很大的負面影響,因為實際上它是通過反射實現的。我們這裡不再通過位元組碼來調研了,記住檢視 scala (或 java)類生成的位元組碼是一件很容易的事情,只需使用 :javap in the Scala REPL ,所以你應該自己試一試。
在我們進入下一個話題之前,再來講一種精煉的使用風格。想象你的結構型別相當的豐富,比如是一個代表某種事物的型別,你可以開啟它,使用它,然後必須關閉。通過使用「型別別名」(在另一部分中有詳細描述)與「結構型別」,我們就可以將型別定義與方法分離,做法如下:
type OpenerCloser = {
def open(): Unit
def close(): Unit
}
def on(it: OpenerCloser)(fun: OpenerCloser => Unit) = {
it.open()
fun(it)
it.close()
}複製程式碼
通過使用這樣一個型別別名,def
的部分變得更加清晰了。我極力推薦這種「對更大的結構型別採用型別別名」的做法,同時也最後提醒大家,確認自己是否真的沒有其它辦法了,再決定採用結構型別。你需要多考慮它負面的效能影響。
22. 路徑依賴型別
這個型別(Path Dependent Type)允許我們對型別內部的型別進行「型別檢查」,這看起來似乎比較奇怪,但下面的例子非常直觀:
class Outer {
class Inner
}
val out1 = new Outer
val out1in = new out1.Inner // concrete instance, created from inside of Outer
val out2 = new Outer
val out2in = new out2.Inner // another instance of Inner, with the enclosing instance out2
// the path dependent type. The "path" is "inside out1".
type PathDep1 = out1.Inner
// type checks
val typeChecksOk: PathDep1 = out1in
// OK
val typeCheckFails: PathDep1 = out2in
// <console>:27: error: type mismatch;
// found : out2.Inner
// required: PathDep1
// (which expands to) out1.Inner
// val typeCheckFails: PathDep1 = out2in複製程式碼
這裡你可以理解為「每個外部類都有自己的內部類」。所以它們是不同的型別 — 差異取決於我們使用哪種路徑獲得。
使用這種型別很有用,我們能夠強制從一個具體引數的內部去獲得型別。一個具體的採用該型別的簽名如下:
class Parent {
class Child
}
class ChildrenContainer(p: Parent) {
type ChildOfThisParent = p.Child
def add(c: ChildOfThisParent) = ???
}複製程式碼
我們現在使用的路徑依賴型別,已經被編碼到了型別系統的邏輯中。這個容器應該只包含這個 Parent
的 Child
物件,而不是任何 Parent
。
我們將很快在 型別投影 章節中看到如何引入任何一個 Parent
的 Child
物件。
23. 型別投影
型別投影(Type Projections)類似「路徑依賴型別」,它們允許你引用一個內部類的型別。在語法上看,你可以組織內部類的路徑結構,然後通過 #
符號分離開來。我們先來看看這些路徑依賴型別(.
語法)和型別投影(#
語法)的第一個且主要的差別:
// our example class structure
class Outer {
class Inner
}
// Type Projection (and alias) refering to Inner
type OuterInnerProjection = Outer#Inner
val out1 = new Outer
val out1in = new out1.Inner複製程式碼
另一個準確的直覺是相比「路徑依賴」,「型別投影」可以用於「型別層面的程式設計」,如 (存在型別)Existential Types。
「存在型別」是跟「型別擦除」密切相關的東西。
val thingy: Any = ???
thingy match {
case l: List[a] =>
// lower case 'a', matches all types... what type is 'a'?!
}複製程式碼
因為執行時型別被擦除了,所以我們不知道 a
的型別。我們知道 List 是一個型別構造器 * -> *
,所以肯定有某個型別,它可以用來構造一個有效的 List[T]
。這個「某個型別」,就是 存在型別。
Scala 為它提供了一種快捷方式:
List[_]
// ^ some type, no idea which one!複製程式碼
假設你在使用一些抽象型別成員,在我們的例子中將會是一些 Monad 。我們想要強制我們的使用者只能使用這個 Monad 中的 Cool
例項,因為比如我們的 Monad 只有針對這些型別才有意義。我們可以通過這些存在型別 T 的型別邊界來實現:
type Monad[T] forSome { type T >: Cool }複製程式碼
mikeslinn.blogspot.com/2012/08/sca…
譯者注:
建議閱讀以下文章,以加深對本部分的理解:
24. Specialized Types
24.1. @specialized
型別專業化(Type specialization)與普通的「型別系統的東西」相比,更多的是一種效能方面的技巧。但如果你想編寫出良好效能的集合,它是非常重要的,我們需要掌握它。舉個例子,我們將實現一個非常有用的集合,稱為 Parcel[A]
,它可以儲存一個指定型別的值 — 確實有用!
case class Parcel[A](value: A)複製程式碼
以上是我們最基本的實現。有什麼問題嗎?沒錯,因為 A
可以是任何東西,所以它就會被表示為一個 Java 物件,就算我們僅對 Int
值進行裝箱。因此上面的類會導致對原始值進行裝箱和拆箱,因為容器正在處理物件:
val i: Int = Int.unbox(Parcel.apply(Int.box(1)))複製程式碼
眾所周知,當你不是真正需要的時候,裝箱不是一個好主意,因為它通過在 int
和 object Int
之間進行來回轉換,產生了更多執行時的工作。怎樣才能消除這個問題呢?一種技巧就是將我們的 Parcel
對所有的原始型別進行「專業化」(這裡拿 Long
和 Int
做例子就夠了),如下:
如果你已經閱讀過 value 類,那麼也許已經注意到
Parcel
可以用它很好地代替實現!確實如此。然而,specialized
在 Scala2.8.1
中就有了,相對地 value 類是在2.10.x
才被引進。並且,前者能夠專業化一種以上的值(雖然它以指數級增長生成程式碼),value 類卻只能限制為一種。
case class Parcel[A](value: A) {
def something: A = ???
}
// specialzation "by hand"
case class IntParcel(intValue: Int) {
override def something: Int = /* works on low-level Int, no wrapping! */ ???
}
case class LongParcel(intValue: Long) {
override def something: Long = /* works on low-level Long, no wrapping! */ ???
}複製程式碼
IntParcel
和 LongParcel
的實現將有效地避開裝箱,因為它們直接在原始值上進行處理,並且無需進入物件領域。現在我們只需根據我們的例項,選擇想要的 *Parcel
。
這看起來很好,但是程式碼基本上變得更難維護了。它有 N
個實現,每種我們需要支援的原始值型別各一個(如包括:int
, long
, byte
, char
, short
, float
, double
, boolean
, void
, 再加上 Object
)! 這需要維護很多樣板。
既然我們已經熟悉了「型別專業化」,也知曉了手動實現它並不是很友好,就來看看 Scala 是如何通過引入 @specialized
註解來幫我們改善這個問題:
case class Parcel[@specialized A](value: A)複製程式碼
如上所示我們將 @specialized
註解應用到了型別引數 A
上,從而指示編譯器生成該類的所有專業化變數,它們是:ByteParcel
, IntParcel
, LongParcel
, FloatParcel
, DoubleParcel
, BooleanParcel
, CharParcel
, ShortParcel
, CharParcel
甚至以及 VoidParcel
(這並不是實際的名字,但你應該明白了大概的意思)。編譯器也同時承擔呼叫正確的版本,所以我們只需要專心寫程式碼,而不必關心一個類是否被專業化了,編譯器會盡可能使用適合的版本(如果有的話):
val pi = Parcel(1) // will use `int` specialized methods
val pl = Parcel(1L) // will use `long` specialized methods
val pb = Parcel(false) // will use `boolean` specialized methods
val po = Parcel("pi") // will use `Object` methods複製程式碼
「太棒了,讓我們盡情使用它吧」 — 這是大部分人發現「專業化」帶來的好處之後的反應,因為它可以在降低記憶體使用率的同時成倍的加速低階操作。不幸的是,它的代價也很高:當使用多個引數時,生成的程式碼量很快變得巨大,就像這樣子:
class Thing[A, B](@specialized a: A, @specialized b: B)複製程式碼
在上面的例子中,我們使用了第二種應用專業化的風格 — 加在引數上,這效果等同於我們直接對 A
和 B
進行專業化。請注意,上述程式碼將生成 8 * 8 = 64
種實現,因為它必須處理如「A 是一個 int
,B是一個 int
」以及「A 是一個 boolean
,但是 B 是一個 long
」的情況 — 你可以看到這是在哪裡。事實上生成的類的數量大約在 2 * 10^(nr_of_type_specializations)
,對於已經有了 3 個型別引數的情況,它很容易達到了數千個類。
有一些方法可以防止這個「指數級爆炸」,例如通過限制專業化的目標型別。假設 Parcel
大部分情況只處理整數,從不跟浮點數打交道,我們就可以編譯器只專業化 Long
和 Int
,如:
case class Parcel[@specialized(Int, Long) A](value: A)複製程式碼
這次讓我們使用 :javap Parcel
來研究一點位元組碼:
// Parcel, specialized for Int and Long
public class Parcel extends java.lang.Object implements scala.Product,scala.Serializable{
public java.lang.Object value(); // generic version, "catch all"
public int value$mcI$sp(); // int specialized version
public long value$mcJ$sp();} // long specialized version
public boolean specInstance$(); // method to check if we're a specialized class impl.
}複製程式碼
如你所見,編譯器提供了額外的專業化方法,如 value$mcI$sp()
,它將返回 int
, long
也有類似的方法。值得一提的是這裡還有另外一個叫做 specInstance$
的方法,如果使用的實現是一個專業化的類,它會返回 true
。
可能你比較好奇當前在 Scala 中哪些類被專業化了,它們有(可能不完整):Function0, Function1, Function2, Tuple1, Tuple2, Product1, Product2, AbstractFunction0, AbstractFunction1, AbstractFunction2 。由於當前專業化 2 個引數的成本已經很高,一個趨勢是我們不要再專業化更多的引數了,雖然我們可以這麼幹。
為什麼我們要避免進行裝箱,一個典型的例子就是「記憶體效率」。想象一個
boolean
值,如果它的儲存只消耗 1 位那是極好的,可惜它不是(包含我瞭解的所有 JVM),例如在 HotSpot 上一個boolean
被當做一個int
,所以它要佔用 4 個位元組的空間。它的兄弟java.lang.Boolean
類似所有 Java 物件一樣,則有 8 位元組的物件頭,然後再儲存boolean
(額外增加 4 位元組)。由於 Java 物件佈局的排列規則,這個物件佔用的空間再分配 16 位元組(8 個位元組給物件頭,4 個位元組給值,4 個位元組給 填充)。這就是為啥我們希望避免裝箱的另外一個悲傷的原因。
24.2. Miniboxing
❌ 該章節作者尚未完成
這不是 Scala 的一個特性,但是可以與 scalac 一起作為編譯器外掛。
我們已經在上一節解釋了,專業化非常強大,但同時也是一個「編譯器炸彈」,具有指數級程式碼增長的問題。現在已經有一個被證實的概念可以解決這個問題,Miniboxing 是一個編譯器外掛,它實現了 @specialized
相同的效果,然而卻不會生成數千個類。
TODO, there’s a project from withing EPFL to make specialization more efficient: Scala Miniboxing
25. Type Lambda
❌ 該章節作者尚未完成
在 type lambda 的部分我們會使用 「路徑依賴型別」及 「結構型別」,如果你忽略了這兩個章節,你可以先跳回去看看。
在瞭解 Type Lambdas 之前,讓我們先回顧下關於「函式」和「柯里化」的某些細節:
class EitherMonad[A] extends Monad[({type λ[α] = Either[A, α]})#λ] {
def point[B](b: B): Either[A, B]
def bind[B, C](m: Either[A, B])(f: B => Either[A, C]): Either[A, C]
}複製程式碼