Scala 類型的類型(四)html
結構類型(Strucural Types)常常被描述爲「類型安全的鴨子類型(duck typing)」,若是你想得到一個直觀的理解,這是一個很好的比較。java
迄今爲止,咱們在類型方面考慮的都是這樣的問題:「它實現了接口 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
)這樣一個方法。它能夠擁有更多的方法,所以這裏並非一個徹底匹配,而是這個類型必須定義最小的一組方法,這樣纔能有效。app
另外須要注意的是,使用結構類型對運行時性能存在很大的負面影響,由於實際上它是經過反射實現的。咱們這裏再也不經過字節碼來調研了,記住查看 scala (或 java)類生成的字節碼是一件很容易的事情,只需使用 :javap in the Scala REPL ,因此你應該本身試一試。ide
在咱們進入下一個話題以前,再來說一種精煉的使用風格。想象你的結構類型至關的豐富,好比是一個表明某種事物的類型,你能夠打開它,使用它,而後必須關閉。經過使用「類型別名」(在另外一部分中有詳細描述)與「結構類型」,咱們就能夠將類型定義與方法分離,作法以下:函數
type OpenerCloser = {
def open(): Unit
def close(): Unit
}
def on(it: OpenerCloser)(fun: OpenerCloser => Unit) = {
it.open()
fun(it)
it.close()
}複製代碼
經過使用這樣一個類型別名,def
的部分變得更加清晰了。我極力推薦這種「對更大的結構類型採用類型別名」的作法,同時也最後提醒你們,確認本身是否真的沒有其它辦法了,再決定採用結構類型。你須要多考慮它負面的性能影響。工具
這個類型(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
對象。
類型投影(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…
建議閱讀如下文章,以加深對本部分的理解:
類型專業化(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 個字節給 填充)。這就是爲啥咱們但願避免裝箱的另一個悲傷的緣由。
❌ 該章節做者還沒有完成
這不是 Scala 的一個特性,可是能夠與 scalac 一塊兒做爲編譯器插件。
咱們已經在上一節解釋了,專業化很是強大,但同時也是一個「編譯器炸彈」,具備指數級代碼增加的問題。如今已經有一個被證明的概念能夠解決這個問題,Miniboxing 是一個編譯器插件,它實現了 @specialized
相同的效果,然而卻不會生成數千個類。
TODO, there’s a project from withing EPFL to make specialization more efficient: Scala Miniboxing
❌ 該章節做者還沒有完成
在 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]
}複製代碼