[譯] Scala 類型的類型(五)

上一篇

Scala 類型的類型(四)html

目錄

21. 結構類型

結構類型(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 的部分變得更加清晰了。我極力推薦這種「對更大的結構類型採用類型別名」的作法,同時也最後提醒你們,確認本身是否真的沒有其它辦法了,再決定採用結構類型。你須要多考慮它負面的性能影響。工具

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) = ???
}複製代碼

咱們如今使用的路徑依賴類型,已經被編碼到了類型系統的邏輯中。這個容器應該只包含這個 ParentChild 對象,而不是任何 Parent

咱們將很快在 類型投影 章節中看到如何引入任何一個 ParentChild 對象。

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)))複製代碼

衆所周知,當你不是真正須要的時候,裝箱不是一個好主意,由於它經過在 intobject Int 之間進行來回轉換,產生了更多運行時的工做。怎樣才能消除這個問題呢?一種技巧就是將咱們的 Parcel 對全部的原始類型進行「專業化」(這裏拿 LongInt 作例子就夠了),以下:

若是你已經閱讀過 value 類,那麼也許已經注意到 Parcel 能夠用它很好地代替實現!確實如此。然而,specialized 在 Scala 2.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! */ ???
}複製代碼

IntParcelLongParcel 的實現將有效地避開裝箱,由於它們直接在原始值上進行處理,而且無需進入對象領域。如今咱們只需根據咱們的實例,選擇想要的 *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)複製代碼

在上面的例子中,咱們使用了第二種應用專業化的風格 — 加在參數上,這效果等同於咱們直接對 AB 進行專業化。請注意,上述代碼將生成 8 * 8 = 64 種實現,由於它必須處理如「A 是一個 int,B是一個 int」以及「A 是一個 boolean,可是 B 是一個 long」的狀況 — 你能夠看到這是在哪裏。事實上生成的類的數量大約在 2 * 10^(nr_of_type_specializations),對於已經有了 3 個類型參數的狀況,它很容易達到了數千個類。

有一些方法能夠防止這個「指數級爆炸」,例如經過限制專業化的目標類型。假設 Parcel 大部分狀況只處理整數,從不跟浮點數打交道,咱們就能夠編譯器只專業化 LongInt,如:

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(),它將返回 intlong 也有相似的方法。值得一提的是這裏還有另一個叫作 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]
}複製代碼
相關文章
相關標籤/搜索