[譯] Scala 型別的型別(三)

ScalaCool發表於2017-05-11

本文由 Yison 發表在 ScalaCool 團隊部落格。

上一篇

Scala 型別的型別(二)

目錄

11. 抽象型別成員

現在我們來深入瞭解「型別別名」的使用場景 — 抽象型別成員(Abstract Type Member)。

有了抽象型別成員,我們就可以說「我希望有人告訴我某個型別 — 我將通過名稱 MyType 來引用它」。抽象型別成員最基本的功能就是讓我們能夠定義泛型類(模板),但卻不是通過使用 class Clazz[A, B] 這種語法,而是在類裡面進行命名,就像這樣子:

trait SimplestContainer { 
type A // Abstract Type Member def value: A
}複製程式碼

學過 Java 的朋友會覺得這在語法上有點類似 Container<
A>
,但在 「路徑依賴型別」 的章節中我們會發現它其實是更強大的,以下的例子也可以說明這一點。

需要注意到的關鍵點是,雖然 Abstract Member Type 命名中包含了關鍵字 abstract,它並不會表現得跟一個「抽象欄位」一樣 — 所以你仍然可以在不「實現」型別成員 A 的前提下建立一個 SimplestContainer 的新例項:

new SimplestContainer // valid, but A is "anything"複製程式碼

你可能會想知道型別 A 到底是什麼,因為我們沒有在任何地方提供關於它的任何資訊。然而實際上 type A 無非只是 type A >
: Nothing <
: Any
的一個縮寫而已,它可以代表「任何東西」。

object IntContainer extends SimplestContainer { 
type A = Int def value = 42
}複製程式碼

我們通過使用型別別名「提供了一個型別」,現在我們可以實現這個 value 方法,返回一個 Int

我們可以對「抽象型別成員」進行約束,這是它更加有趣的應用。假設你想要一個容器,只能儲存一個 Number 的任何示例。我們可以在定義一個抽象型別成員的地方註釋以下的約束:

trait OnlyNumbersContainer { 
type A <
: Number def value: A
}複製程式碼

或者我們可以稍後在類的繼承關係中新增約束,比如繼承一個宣告「only Numbers」的特質:

trait SimpleContainer { 
type A def value: A
}trait OnlyNumbers {
type A <
: Number
}val ints = new SimpleContainer with OnlyNumbers {
def value = 12
}// bellow won't compileval _ = new SimpleContainer with OnlyNumbers {
def value = "" // error: type mismatch;
found: String("");
required: this.A

}複製程式碼

因此,就如你看到的,我們可以像使用「型別變數」一樣使用「抽象型別成員」,但是卻不必像前者一樣到處顯式傳遞,因為它不是一個欄位。雖然這裡需要付出一點代價 — 我們需要給這些型別取名字。

12. 自遞迴型別

自遞迴型別(Self-recursive Types)在大多數文獻中被稱為 F-Bounded Types 。所以你可能會發現很多文章或部落格引用 F-bounded 。事實上,這是 self-recursive 的另一種叫法,代表了「子型別約束」本身是通過引數化發生在左側的繫結器的情況。

由於「自遞迴」的叫法更加直觀,我們會在後續的文中堅持使用(儘管還是有部分讀者會在 google 中搜尋「F-bounded是什麼」)。

12.1 F-Bounded Type

雖然這不是 Scala 的某種具體型別,但它有時也讓人感到棘手。很多人熟悉(也許是不知不覺地)的一個自遞迴型別的例子是 Java 中的 Enum<
E>
。如果你比較好奇,可以參見 Enum sources from Java 。但現在先讓我們回到 Scala,看看我們到底在討論什麼。

在本節中,我們不會特別深入探討這種型別。如果你想要了解在 Scala 中更多、更深入的用例,或許可以看看 Kris Nuttycombe 的 F-Bounded Type Polymorphism Considered Tricky

想象你有某個 Fruit 特質,一個 AppleOrange 繼承了它。Fruit 特質同時還有一個 compareTo 方法,這時候問題出現了 — 猜想你想說「我不能拿橘子和蘋果進行比較啊,它們可是完全不同的東西!」。我們先來寫一段天真的實現程式碼:

// naive impl, Fruit is NOT self-recursively parameterisedtrait Fruit { 
final def compareTo(other: Fruit): Boolean = true // impl doesn't matter in our example, we care about compile-time
}class Apple extends Fruitclass Orange extends Fruitval apple = new Apple()val orange = new Orange()apple compareTo orange // compiles, but we want to make this NOT compile!複製程式碼

在這段程式碼中,由於 Fruit 特質不知道誰會繼承它,所以不可能通過限制 compareTo 的簽名來實現只允許傳入跟this 相同的子型別引數。讓我們利用「自遞迴型別引數」來重新實現下:

trait Fruit[T <
: Fruit[T]]
{
final def compareTo(other: Fruit[T]): Boolean = true // impl doesn't matter in our example
}class Apple extends Fruit[Apple]class Orange extends Fruit[Orange]val apple = new Appleval orange = new Orange複製程式碼

注意 Fruit 簽名裡的型別引數,你可以解讀為「我傳入了型別 T , T 必須是一個 Fruit[T]」,必須像上述 AppleOrange 一樣繼承這個特質才能滿足這種界限條件。現在如果我們要比較 appleorange ,我們就會得到一個編譯時錯誤:

scala>
orange compareTo apple:13: error: type mismatch;
found : Apple required: Fruit[Orange] orange compareTo applescala>
orange compareTo orangeres1: Boolean = true複製程式碼

因此我們確定只能在同類水果之間進行比較,比如蘋果跟蘋果。假使討論更多,要是 AppleOrange 的子類呢?好,因為我們在型別繼承關係中在 Apple / Orange 層填寫了型別引數,根本上我們可以說「蘋果只能跟蘋果進行比較」,這也意味著蘋果的子類可以進行相互比較。這對 Fruit 的 compareTo 的簽名來說依舊好辦,因為我們呼叫的右側部分會變成 Fruit[Apple] — 變得更具體一點而已。讓我們用一個日本的蘋果(ja. "
りんご"
, "
ringo"
)和一個波蘭的蘋果(pl. "
Jabłuszko"
)舉例:

object `りんご`  extends Appleobject Jabłuszko extends Apple`りんご` compareTo Jabłuszko// true複製程式碼

你也可以通過其它奇技淫巧來實現同樣的型別安全,比如路徑依賴型別、隱式引數或 Type Class ,但這應該是最簡單的實現方式。

13. 型別構造器

❌ 該章節作者尚未完成,或需要修改

型別構造器跟函式幾乎是類似的,但前者是在型別層面。換句話說,你在日常的程式設計中可以給一個函式傳入一個值 a,然後返回一個值 b 。於是在型別層面程式設計,你可以認為一個 List[+A] 是一個型別構造器,表現如下:

  • List[+A] 有一個型別引數 (A);
  • 它本身並不是一個有效的型別,你需要填充 A 所在的地方來「構造型別」;
  • 填上 Int 你就得到了一個具體的型別 List[Int]

通過這個例子,你會發現「型別構造器」跟「普通構造器」是如此的相似,唯一不同的地方在於前者處理的是型別,而不是物件的例項。值得注意的是,在 Scala 中我們不能說某個東西的型別是 List ,因為它並不像 Java 裡,javac 會將 List<
Object>
給你。 Scala 在這個地方是更嚴格的,它並不允許我們僅僅使用一個 List 來代表一個型別,因為它是一個型別構造器,而不是一個真正的具體型別。

在 Scala 2.11.x 中我們將在 REPL 中擁有一個強大的命令 – :kind ,它支援檢測一個型別是高階。讓我們通過一個簡單的型別構造器來試試看,比如 List[+A]

// Welcome to Scala version 2.11.0-M5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0-ea).// Type in expressions to have them evaluated.:kind List// scala.collection.immutable.List's kind is F[+A]:kind -v List// scala.collection.immutable.List's kind is F[+A]// * -(+)->
*
// This is a type constructor: a 1st-order-kinded type.複製程式碼

這裡我們看到,scalac 可以告訴我們 List 實際上是一個型別構造器(當與 -verbose 一起使用時,會更有說服力)。我們來調查下上述資訊中的語法:* ->
*
。這個語法被廣泛地用於代表型別( kind ,而不是 type ),我發現事實上這是受到了 Haskell 的啟發 — Haskell 用它來列印函式的型別。最直觀的解讀是「傳入一個型別,返回另一個型別」。你也許已經注意到我們從 Scala 完整的輸出中省略了來自關係中的 + 符號(* -(+)->
*
)。這個代表型變的邊界,你可以在 Scala 中的型變 一節中瞭解更多關於型變的內容。

綜上所述,List[A+] (或者 Option[+A] ,或者 Set[+A] …… 或者其它有一個型別引數的東西)是最簡單的型別構造器的例子 — 這些都有一個引數。我們稱它們為第一階型別 (* ->
*
)。值得一提的是,一個 Pair[+A, +B] (我們可以表示為 * ->
* ->
*
)依舊不是一個「高階型別」,它也是第一階的。在下一節中,我們將仔細研究高階型別到底給我們帶來了什麼,以及如何識別它。

14. 高階型別

❌ 該章節作者尚未完成,仍舊缺失部分內容

這裡一個典型的例子是 Monad

scala>
import scalaz._import scalaz._scala>
:k Monad // Finds locally imported types.Monad's kind is (* ->
*) ->
*This is a type constructor that takes type constructor(s): a higher-kinded type.複製程式碼

TODO:blogs.atlassian.com/2013/09/sca…

15. 樣例類

樣例類(Case Class)是 Scala 編譯器中最有用的小技巧之一。它使用起來簡單,然而又幫了大忙。它為我們避免了一些非常重複無聊的工作,如 equalshashCodetoString 的實現,內建了 apply / unapply 方法來支援模式匹配,等等。

在 Scala 中一個樣例類的宣告就像一個普通的類一樣,只是需要前置一個 case 關鍵字:

case class Circle(radius: Double)複製程式碼

僅一行程式碼,我們就已經實現了 Value Object 模式。這意味著通過定義一個樣例類,我們就自動做到了以下事情:

  • 它的例項是不可變的;
  • 可以使用 equals 來被比較,通過它的欄位來判定相等(而不是類似一個普通類的物件相等);
  • 它的 hashCode 奉行 equals 的契約,是基於類的值;
  • 它的 toString 由類名和它所包含的欄位的值組成的(對照上面的 Circle,可實現為 def toString = s"
    Circle($radius)"
    )。

我們消化下目前所提到的東西,接下來將使用一個生動的例子來繼續延伸。這次我們要實現一個 Point 類,它會擁有不止一個欄位,來展現 case class 給我們提供的一些有趣的特性:

case class Point(x: Int, y: Int)        val a = Point(0, 0)                   ③ // a.toString == "Point(0,0)"         val b = a.copy(y = 10)                  // b.toString == "Point(0,10)"⑤ a == Point(0, 0)複製程式碼

xy 自動被定義為 val 成員;

② 一個 Point 的伴身物件會同時產生,它有一個 apply(x: Int, y: Int) 方法,我們可以藉此建立例項;

③ 生成的 toString 方法包含了類名以及 case class 的引數值;

copy(...) 方法支援我們輕鬆建立拷貝的物件,改變選定的欄位;

⑤ case class 基於值來判等 ( equalshashCode 被生成,它們基於 case class 的引數實現)

除此之外,一個 case class 還可被用於模式匹配,使用慣常的或者「抽取器模式」語法:

Circle(2.5) match { 
case Circle(r) =>
println("Radius = " + r)
}val Circle(r)val r2 = r + r複製程式碼

來源:https://juejin.im/post/5913d76144d904006c3b47ab#comment

相關文章