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

上一篇

Scala 類型的類型(三)html

目錄

16. 枚舉

Scala 中並無像 Java 同樣支持枚舉語法,但咱們可使用一些技巧(包含在 Enumeration)來寫出相似的東西。java

16.1. Enumeration

在 Scala 2.10.x 版本中能夠經過使用 Enumeration 來實現「相似枚舉」的結構。git

object Main extends App {

① object WeekDay extends Enumeration {               
②    type WeekDay = Valueval Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value    
  }
④  import WeekDay._                                   

  def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)

⑤  WeekDay.values filter isWorkingDay foreach println 
}複製代碼

① 首先咱們聲明一個單例來包含咱們的枚舉值,它必須繼承 Enumerationgithub

② 在這裏,咱們爲 Enumeration 內部的 Value 類型定義一個 類型別名 ,由於咱們須要取一個匹配單例名字的名字,後面能夠始終經過「WeekDay」來引用它。(是的,這幾乎是一個 hack )json

③ 在這裏,咱們採用了「多重賦值」,所以每一個 val 左邊的變量都被賦值了一個不一樣的 Value 類型的實例數組

④ 這個 import 帶來了兩點:不只支持了在沒有 WeekDay 前綴的狀況下直接使用 Mon ,同時也在這個做用域中引入了 type WeekDay ,因而咱們能夠在下方的方法定義中使用它安全

⑤ 最後,咱們得到了一些 Enumeration 的方法,這些並非魔術,當咱們建立新的 Value 實例時,大部分動做都會發生工具

正如你所見,Scala 中的枚舉機制並非內置的,而是經過巧妙地藉助類型系統來實現。對於一些使用場景,這也許已經足夠了。但當遇到須要增長枚舉值以及往每一個值增長行爲的時候,它就不能像 Java 那樣強大了。性能

16.2. @enum

@enum 註解如今已經不只僅只是一個提議了, 已經處在 Scala 內部的討論進程中了:Enumeration must DIE...學習

@enum 註解可能會跟「註解宏」一塊兒,在未來被支持。在 Scala 改進計劃文檔中有關於此的描述:enum-sip

@enum
class Day {
  Monday    { def goodDay = false }
  Tuesday   { def goodDay = false }
  Wednesday { def goodDay = false }
  Thursday  { def goodDay = false }
  Friday    { def goodDay = true  }
  def goodDay: Boolean
}複製代碼

譯者注:做者以上說起的方案已被官方棄用,但 enum 關鍵字將在 Dotty 中被支持,參見 dotty.epfl.ch/docs/refere…

17. value 類

value 類型(Value Class)在 Scala 內部存在了很長時間,而且你也已經使用過它們不少次了。由於 Scala 中全部的 Number 都使用這個編譯器技巧來避免數字值的裝箱和拆箱的過程,好比從 intscala.Int 等。提醒下你回想一下 Array[Int] ,它其實在 JVM 中是 int[] ,(若是你對 bytecode 熟悉,會知道它是 JVM 的一種運行時類型:[I])它 會有蠻多性能方面的影響。總的來講,數字的數組性能很好,但引用的數組就沒那麼快了。

好的,咱們如今知道了編譯器能夠在沒必要要的時候經過奇技淫巧來避免將 ints 裝箱成 Ints 。所以讓咱們來看看 Scala 在 2.10.x 以後是如何將這個特性展現給咱們的。這個特性被稱爲「value 類」,能夠至關簡單地應用到你現有的類當中。使用它們簡單到只要把 extends AnyVal 加到你的類中,同時遵循如下將說起的新規則。若是你不熟悉 AnyVal ,這多是一個很好的學習機會 — 你能夠查看 通用類型系統 — Any, AnyRef, AnyVal

讓咱們實現一個 Meter 來做爲咱們的例子,它將實現一個原生 int 的包裝 ,並支持將以「meter」爲單位的數字轉化爲以 Foot 類型的數字。咱們須要上一課,由於沒人理解皇室的制度 ;-) 。不過,若是 95% 的時候都使用原生的 meter 值,爲何咱們要由於讓一個對象包含一個 int 而支付額外的運行時開銷?(每一個實例都有好幾個字節!)是由於這是一個面向歐洲市場的項目?咱們須要「value 類」的救援!

case class Meter(value: Double) extends AnyVal {
  def toFeet: Foot = Foot(value * 0.3048)
}

case class Foot(value: Double) extends AnyVal {
  def toMeter: Meter = Meter(value / 0.3048)
}複製代碼

咱們將在全部的例子中使用樣例類(value 類),但它在技術上不是硬性要求的(儘管很是方便)。雖然你也能夠經過在一個普通類使用 val 來實現一個 value 類,相比樣例類一般會是最佳方案。你可能會問「爲何只有一個參數」,這是由於咱們會盡可能避免去包裝值,這對於單個值是有意義的,不然咱們就必須在某些地方保持一個元組,這樣很快就會變得含糊,同時咱們也將失去「不包裝」策略下的性能。所以記住,value 類僅適用於一個值,雖然沒人能夠說這個參數必須是一個原始類型,它也能夠是一個普通類,如 FruitPerson ,咱們有時候依舊能夠避免在 value 類中進行包裝。

全部你在定義一個 value 類時須要作的,就是擁有一個包含「繼承 AnyVal變量」的類,同時遵循一些它的限制。這個變量不必定就是原始類型,它能夠是任何東西。這些限制換句話說,就是一個更長的列表,好比一個 value 類型不能包含除了 def 成員外的其它字段,而且不能被擴展,等等。完整的限制清單以及更深刻的例子,能夠參加 Scala 文檔 — [Value Classes - summary of limitations])(docs.scala-lang.org/overviews/c…) 。

好了,如今咱們擁有了 MeterFoot 值樣例類,咱們首先檢查下當添加了 extends AnyVal 部分以後,生成的字節碼如何使 Meter 從一個普通的樣例類,變成一個 value 類:

// case class
scala> :javap Meter

public class Meter extends java.lang.Object implements scala.Product,scala.Serializable{
    public double value();
    public Foot toFeet();
    // ...
}

scala> :javap Meter$
public class Meter$ extends scala.runtime.AbstractFunction1 implements scala.Serializable{
    // ... (skipping not interesting in this use-case methods)
}複製代碼

爲 value 類生成的字節碼以下:

// case value class

scala> :javap Meter
public final class Meter extends java.lang.Object implements scala.Product,scala.Serializable{
    public double value();
    public Foot toFeet();
    // ...
}

scala> :javap Meter$
public class Meter$ extends scala.runtime.AbstractFunction1 implements scala.Serializable{
    public final Foot toFeet$extension(double);
    // ...
}複製代碼

有一件事情應該引發咱們的重視,就是當 Meter 做爲一個 value 類被建立時,它的伴生對象得到了一個新的方法 — toFeet$extension(double): Foot 。在這個方法成爲 Meter 類的實例方法以前,它沒有任何參數(因此它是:toFeet(): Foot)。生成的方法被標記爲「extension」(toFeet$extension),實際上這也是咱們給這些方法所取得名字。( .NET 開發者已經看到這種趨勢了)

因爲咱們的 value 類的目標是避免必須分配整個 value 類對象,從而直接跟包裝後的值打交道,因此咱們必須中止使用實例方法,由於它們將迫使咱們產生一個包裝( Meter )類的實例。咱們能作的事情是,將這個實例方法變成一個「擴展方法」,它將存儲在 Meter 的伴生對象中。咱們經過傳入 Double 類型值,而不是使用實例的 value: Double 來調用這個擴展方法。

擴展方法的做用跟隱式轉換相似(後者是一個更通用,以及更強大的武器),但它是更加簡單的一種方式 — 避免了必須分配整個包裝後的對象。相對的,隱式轉換會須要它來提供「額外的方法」。擴展方法有點採用「重寫生成的方法」的路線,以便它們將「要擴展的類型」做爲它們第一個參數。舉個例子,假如你寫了 3.toHexString ,這個方法會經過一個隱式轉換被添加到 Int ,然而因爲目標是 class RichInt extends AnyVal ,因此一個 value 類的調用並不會致使 RichInt 的分配,而是會被重寫成 RichInt$.$MODULE$.toHexString$extension(3),這樣子就避免了 RichInt 的分配。

讓咱們用新學習到的知識來調查下在 Meter 的例子中,編譯器到底爲咱們作了什麼。源碼旁邊註釋的部分解釋了編譯器實際上生成的東西。(如此來發現代碼運行時發生了什麼):

// source code // what the emited bytecode actualy doesval m: Meter  = Meter(12.0)    // store 12.0 val d: Double = m.value * 2    // double multiply (12.0 * 2.0), store val f: Foot   = m.toFeet       // call Meter$.$MODULE$.toFeet$extension(12.0)複製代碼

① 有人可能會期待在這裏分配一個 Meter 對象,然而因爲咱們正在使用一個 value 類,只有被包裝的值被存儲 — 即咱們在運行時一直在處理的一個原生 double 值。(賦值和類型檢查依舊會驗證這是否個 Meter 實例)

② 在這裏,咱們訪問了 value 類的 value(這個字段名的名字沒有關係)。請注意,運行時這裏操做的是原生的 doubles ,所以沒必要像往常一個普通的樣例類同樣,調用一個 value 的方法。

③ 這裏,咱們彷佛在調用一個定義在 Meter 裏的實例方法,然而事實上,編譯器已經用一個擴展方法調用代替了這個調用,它在 12.0 這個值中傳遞。咱們得到了一個 Foot 實例… 等一下!可是 Foot 這裏也被定義成了一個 value 類,因此在運行時咱們再次獲得了一個原生 double

這些都是「擴展方法」和 「value 類」的基礎知識。若是你想閱讀更多,瞭解不一樣的邊界狀況,請參考官方關於 value 類的章節,Mark Harrah 在這裏用了不少例子,解釋得很好。因此除了基本介紹外,我就再也不重複勞動了。

18. 類型類

❌ 該章節做者還沒有完成,或須要修改

類型類(Type Class)屬於 Scala 中可利用的最強大的模式,能夠總結爲(若是你比較喜歡華麗的措施)「特定多態」。等到本章結束以後,你就能夠理解它了。

Scala 爲咱們解決的典型的問題就是,在無需顯式綁定兩個類的前提下,提供可拓展的 API 。舉一個嚴格綁定的例子,咱們不使用類型類,如擴展一個 Writable 接口,爲了讓咱們自定義的數據類型可寫:

// no type classes yet
trait Writable[Out] {
  def write: Out
}

case class Num(a: Int, b: Int) extends Writable[Json] {
  def write = Json.toJson(this)
}複製代碼

使用這種風格,只是擴展和實現了一個接口,咱們將 Num 轉化爲 Writable ,同時咱們也必須提供 write 的實現,「必須在這裏立刻實現」,這使得其餘人難以提供不一樣的實現 — 它們必須繼承實現一個 Num 子類。這裏的另外一個痛點是,咱們不能從一個相同的特質繼承兩次,提供不一樣的序列化目標(你不能同時繼承 Writable[Json]Writable[Protobuf])。

全部這些問題均可以經過基於類型類的方法解決,而不是直接繼承 Writable[Out] 。讓咱們試一試,並詳細解釋下這究竟是如何作的:

trait Writes[In, Out] {                                               
    def write(in: In): Out
  }

② trait Writable[Self] {                                               
    def write[Out]()(implicit writes: Writes[Self, Out]): Out =
      writes write this
  }

③ implicit val jsonNum = Writes[Num, Json] {                            
    def (n1: Num, n2: Num) = n1.a < n1.
  }

case class Num(a: Int) extends Writable[Num]複製代碼

① 首先咱們定義下類型類,它的 API 跟以前的 Writable 特質相似,但咱們保持分離,而不是將它們混合到一個寫入的類中。這是爲了知道咱們用「自類型註解」定義了什麼

② 接下來咱們將咱們的 Writable 特質改成使用 Self 進行參數化,並將「目標序列化類型」移動到 write 的簽名中。它如今還須要一個隱式的 Writes[Self, Out] 實現,它將處理序列化 — 這就是咱們的類型類

③ 這是類型類的實現。請注意,咱們將實例標記爲 implicit ,因此

Universal traits 是 extend Any 的特質,它們應該只有 def ,而且沒有初始化代碼。

這裏做者還須要有不少補充

19. 自身類型註解

「自身類型」(Self Types)可被用來給一個特質混入外部類型,若是一個其餘的類使用了這個特質,它也必須提供該特質混入部分的實現。

來看一個例子,該例子中 Service 特質混入了 Module 特質,後者內部提供了其它的 services。咱們能夠經過以下的「自身類型註解」來表示:

trait Module {
  lazy val serviceInModule = new ServiceInModule
}

trait Service {
  this: Module =>

  def doTheThings() = serviceInModule.doTheThings()
}複製代碼

Service 定義部分的第二行能夠被閱讀爲「 I’m a Module 」。這看起來與繼承一個 Module 並沒什麼兩樣,究竟哪裏不一樣呢?

前者意味着咱們必須在實例化一個 Service 的同時也提供 Module

trait TestingModule extends Module { /*...*/ }

new Service with TestingModule複製代碼

若是你沒有混入所需的特質,就會像以下同樣失敗:

new Service {}

// class Service cannot be instantiated because it does not conform to its self-type Service with Module
// new Service {}
// ^複製代碼

同時你也應該瞭解,咱們能夠利用「自身類型」語法混入多個特質。寫到這,讓咱們討論下爲何它被叫作 self-type(除了「是的,它看起來很通」的因素)。答案可能要歸於這仍是一種流行的使用風格,就像下面這樣:

class Service {
  self: MongoModule with APIModule =>

  def delegated = self.doTheThings()
}複製代碼

事實上,你可使用任何標識符(不只僅是 this 或者 self),而後在你的類中引用它。

20. 幽靈類型

幽靈類型(Phantom Types)儘管是個古怪的名字,但彷佛很是貼切。它能夠被解釋爲「不是實例的類型」,咱們不直接使用它們,可是能夠用來執行一些更嚴格的邏輯。

咱們要舉的例子是一個 Service 類,它有 startstop 方法。如今咱們想要確保你不能「開始」一個已經在運行的服務(類型系統不容許這麼幹),反之亦然。

一開始咱們先來定義一些標記狀態的特質,它們不包含任何邏輯,咱們只會用它們來表示一個服務的狀態類型:

sealed trait ServiceState
final class Started extends ServiceState
final class Stopped extends ServiceState複製代碼

注意這裏給 ServiceState 特質採用了 sealed 來確保不會有人在系統裏忽然增長其它狀態。同時咱們也把子類型定義爲 final ,所以它們也不會被繼承,系統不會被引入其它更多狀態。

關於 sealed 關鍵詞

sealed 確保全部繼承一個類或者特質的行爲都必須在相同的一個編譯單元。舉例而言,若是你在一個 State.scala 的文件裏定義了 sealed trait State 以及一些狀態實現,這沒毛病。然而,若是你不能再其它的文件來繼承 State (如 MyStates.scala)。

注意了,以上狀況只針對使用了 sealed 關鍵詞的類型有效,但不適用於它的子類。若是你不能在其它文件裏繼承 State ,可是若是你準備了一個類型如 trait UserDefinedState extends State ,咱們則能夠定義更多 UserDefinedState 的子類,即便是經過其它的文件。假如你要阻止這樣的狀況發生,你應該給你的子類們加上 final,正如咱們在以上例子中所作的。

瞭解了這些,咱們終於能夠來研究如何將它們做爲幽靈類型來使用。首先咱們先來定義一個 Service 類,它有一個 State 類型參數。這裏請注意了,咱們將不會在這個類中使用任何 State 類型的值!它只是靜靜地在這裏,像一個幽靈 —— 這也是它名字的由來。

class Service[State <: ServiceState] private () {
  def start[T >: State <: Stopped]() = this.asInstanceOf[Service[Started]]
  def stop[T >: State <: Started]() = this.asInstanceOf[Service[Stopped]]
}
object Service {
  def create() = new Service[Stopped]
}複製代碼

所以在這個伴身對象裏,咱們先建立了一個 Service 的實例,在最開始它的狀態是 Stopped 。這個狀態也符合類型參數(<: ServiceState)的類型邊界,這很好。

當咱們想要開始/中止一個已經存在的 Service 的時候,有趣的事情來了。好比在 start 方法裏定義的這個類型邊界,只針對一個 T 的值有效,也就是 Stopped 。在咱們的例子中,進行狀態切換是一個空操做,它仍是會返回相同的實例,同時顯式地轉化爲所須要的狀態。因爲這個類型沒有被任何東西調用,你也不會在這個操做中遇到類轉換異常。

如今咱們使用 REPL 來調查下以上的代碼,做爲本章節一個很好的收場:

① scala> val initiallyStopped = Service.create() 
  initiallyStopped: Service[Stopped] = Service@337688d3

②  scala> val started = initiallyStopped.start()  
  started: Service[Started] = Service@337688d3

③  scala> val stopped = started.stop()            
  stopped: Service[Stopped] = Service@337688d3

④  scala> stopped.stop()                          
  <console>:16: error: inferred type arguments [Stopped] do not conform to method stop's
                     type parameter bounds [T >: Stopped <: Started]
              stopped.stop()
                      ^

⑤  scala> started.start()                         
  <console>:15: error: inferred type arguments [Started] do not conform to method start's
                     type parameter bounds [T >: Started <: Stopped]
              started.start()複製代碼

① 這裏咱們建立了一個初始化實例,它開始的狀態是 Stopped
② 成功開啓一個 Stopped 的 service,返回的類型爲 Service[Started]
③ 成功結束一個 Started 的 service,返回的類型爲 Service[Stopped]
④ 然而結束一個已經中止的 service (Service[Stopped])是無效的,不能經過編譯,注意打印出來的類型邊界
⑤ 相似地,結束一個已經開始的 service (Service[Started])是無效的,不能經過編譯,注意打印出來的類型邊界

正如你所看到的,幽靈類型是另外一種強大的工具,可讓咱們的代碼更加的類型安全(或者我應該說「狀態安全」!?)。

若是你好奇哪些「不是過於瘋狂的類庫」使用了這些特性,這裏一個值得推薦的例子是 Foursquare Rogue(the MongoDB query DSL)。它利用幽靈類型來確保一個 query builder 的狀態正確性,如 limit(3) 被正確地調用。

相關文章
相關標籤/搜索