適用於 Java 程式設計師的 Scala 教學課程

語言

如果您已經具備一些 Java 經驗,準備開始使用 Scala,本頁面將提供差異的良好概觀,以及在開始使用 Scala 程式設計時會遇到的情況。為了獲得最佳效果,我們建議您在電腦上設定 Scala 工具鏈,或使用 Scastie 在瀏覽器中編譯 Scala 程式片段

快速瀏覽:為什麼選擇 Scala?

沒有分號的 Java:有一句諺語說 Scala 是沒有分號的 Java。這句話有很大的道理:Scala 簡化了 Java 的許多雜訊和樣板,同時建立在相同的基礎上,共享相同的底層類型和執行時間。

無縫互操作:Scala 可以直接使用任何 Java 函式庫!包括 Java 標準函式庫!而且幾乎任何 Java 程式在 Scala 中都能以相同的方式執行,只需轉換語法即可。

可擴充的語言:Scala 的名稱來自於可擴充語言。Scala 不僅可以隨著硬體資源和負載需求進行擴充,還可以隨著程式設計師的技能水平進行擴充。如果你選擇,Scala 會獎勵你表達性的附加功能,與 Java 相比,這些功能可以提高開發人員的生產力和程式碼的可讀性。

它會隨著你成長:學習這些額外的內容是你可以按照自己的步調進行的選修步驟。我們認為,最有趣且最有效的方法是,首先確保你使用從 Java 中獲得的知識來提高生產力。然後,按照 Scala 書籍 的說明,一次學習一件事。選擇適合你的學習步調,並確保你所學習的內容是有趣的。

TL;DR:你可以開始編寫 Scala,就好像它是具有新語法的 Java 一樣,然後根據需要從那裡進行探索。

下一步

比較 Java 和 Scala

本教學課程的其餘部分將詳細說明 Java 和 Scala 之間的一些主要差異。如果你只想快速參考兩者之間的差異,請閱讀Java 開發人員的 Scala,其中包含許多範例,你可以在你選擇的 Scala 設定中試用這些範例

進一步探索

完成這些指南後,我們建議你透過閱讀Scala 書籍或參加一些線上大型開放式課程來繼續你的 Scala 之旅。

你的第一個程式

撰寫 Hello World

作為第一個範例,我們將使用標準的 Hello World 程式。它並不是很吸引人,但可以輕鬆示範 Scala 工具的使用方式,而無需對語言有太多了解。以下是它的外觀

object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello, World!")
  }
}

此程式的結構應該讓 Java 程式設計師感到熟悉:它的進入點包含一個名為 main 的方法,它將命令列引數(一個字串陣列)作為參數;此方法的主體包含對預先定義的方法 println 的單一呼叫,並將友善的問候語作為引數。 main 方法不會傳回值。因此,其傳回類型宣告為 Unit(等同於 Java 中的 void)。

對於 Java 程式設計師來說,較不熟悉的是包含 main 方法的 object 宣告。此宣告引入了通常稱為單例物件的內容,也就是只有一個執行個體的類別。因此,以上的宣告同時宣告了一個名為 HelloWorld 的類別,以及該類別的一個執行個體,也稱為 HelloWorld。此執行個體會在需要時(第一次使用時)建立。

與 Java 的另一個不同點是,main 方法在此並未宣告為 static。這是因為 Scala 中不存在靜態成員(方法或欄位)。Scala 程式設計師不會定義靜態成員,而是將這些成員宣告在單例物件中。

@main def HelloWorld(args: String*): Unit =
  println("Hello, World!")

此程式的結構可能讓 Java 程式設計師感到陌生:沒有名為 main 的方法,而是透過新增 @main 註解,將 HelloWorld 方法標記為進入點。

程式進入點可以選擇性地接受參數,這些參數會由命令列引數填入。在此,HelloWorld 會將所有引數擷取到一個名為 args 的可變長度字串序列中。

方法主體包含對預定義方法 println 的單一呼叫,並以友善問候語作為引數。 HelloWorld 方法不傳回值。因此,其傳回類型宣告為 Unit(等同於 Java 中的 void)。

對於 Java 程式設計師來說,更不熟悉的是 HelloWorld 不需要包覆在類別定義中。Scala 3 支援頂層方法定義,這對於程式進入點來說是理想的。

方法也不需要宣告為 static。這是因為靜態成員(方法或欄位)在 Scala 中不存在。相反地,頂層方法和欄位是其封閉套件的成員,因此可以在程式中的任何地方存取。

實作細節:為了讓 JVM 可以執行程式, @main 註解會產生一個類別 HelloWorld,其中包含一個靜態 main 方法,該方法會呼叫 HelloWorld 方法,並帶有命令列引數。此類別僅在執行階段可見。

執行 Hello World

注意:以下假設您在命令列上使用 Scala

從命令列編譯

若要編譯範例,我們使用 Scala 編譯器 scalacscalac 的運作方式與大多數編譯器類似:它將原始檔作為引數,可能還會加上一些選項,並產生一個或多個輸出檔。它產生的輸出是標準的 Java 類別檔。

如果我們將上述程式儲存在名為 HelloWorld.scala 的檔案中,我們可以透過發出以下命令來編譯它(大於符號 > 代表殼層提示字元,不應輸入)

> scalac HelloWorld.scala

這會在目前的目錄中產生幾個類別檔。其中一個會命名為 HelloWorld.class,其中包含一個類別,可以使用 scala 命令直接執行,如下一個區段所示。

從命令列執行

編譯後,可以使用 scala 指令執行 Scala 程式。它的用法與用於執行 Java 程式的 java 指令非常類似,並接受相同的選項。可以使用下列指令執行上述範例,它會產生預期的輸出

> scala -classpath . HelloWorld

Hello, World!

使用 Java 函式庫

Scala 的優點之一是它讓與 Java 程式碼互動變得非常容易。預設會匯入 java.lang 套件中的所有類別,而其他類別則需要明確匯入。

我們來看一個展示此功能的範例。我們想要取得並格式化當前日期,並根據特定國家/地區(例如法國)使用的慣例進行格式化。(瑞士的法語區等其他地區使用相同的慣例。)

Java 的類別函式庫定義了強大的公用程式類別,例如 LocalDateDateTimeFormatter。由於 Scala 可以與 Java 無縫互通,因此無需在 Scala 類別函式庫中實作等效類別;我們可以匯入對應 Java 套件的類別

import java.time.format.{DateTimeFormatter, FormatStyle}
import java.time.LocalDate
import java.util.Locale._

object FrenchDate {
  def main(args: Array[String]): Unit = {
    val now = LocalDate.now
    val df = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(FRANCE)
    println(df.format(now))
  }
}

Scala 的 import 陳述式看起來與 Java 的等效陳述式非常類似,但功能更強大。可以透過將多個類別括在第一行的花括號中,從同一個套件匯入這些類別。另一個差異是,在匯入套件或類別的所有名稱時,在 Scala 2 中我們使用底線字元 (_) 而不是星號 (*)。

import java.time.format.{DateTimeFormatter, FormatStyle}
import java.time.LocalDate
import java.util.Locale.*

@main def FrenchDate: Unit =
  val now = LocalDate.now
  val df = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(FRANCE)
  println(df.format(now))

Scala 的 import 陳述式看起來與 Java 的等效陳述式非常類似,但功能更強大。可以透過將多個類別括在第一行的花括號中,從同一個套件匯入這些類別。與 Java 相同,在 Scala 3 中我們使用星號 (*) 匯入套件或類別的所有名稱。

因此,第三行的 import 陳述式會匯入 Locale 列舉的所有成員。這會讓靜態欄位 FRANCE 直接可見。

在進入點方法中,我們首先建立一個 Java DateTime 類別的執行個體,其中包含今天的日期。接著,我們使用 DateTimeFormatter.ofLocalizedDate 方法定義一個日期格式,傳遞 LONG 格式樣式,然後再傳遞先前匯入的 FRANCE 地區設定。最後,我們列印根據在地化 DateTimeFormatter 執行個體格式化的當前日期。

為結束本節關於與 Java 整合的說明,應注意,也可以直接在 Scala 中繼承 Java 類別並實作 Java 介面。

旁注:第三方函式庫

標準函式庫通常不夠用。作為 Java 程式設計師,您可能已經知道許多您想在 Scala 中使用的 Java 函式庫。好消息是,與 Java 一樣,Scala 的函式庫生態系統建立在 Maven 座標之上。

大多數 Scala 專案都是使用 sbt 建置的:新增第三方函式庫通常由建置工具管理。來自 Java 的您可能熟悉 Maven、Gradle 和其他此類工具。仍然可以使用這些工具來建置 Scala 專案,但一般會使用 sbt。請參閱使用 sbt 設定 Scala 專案,以取得有關如何使用 sbt 建置專案並新增一些依賴項的指南。

所有東西都是物件

Scala 是一種純物件導向語言,其意義在於所有東西都是物件,包括數字或函式。它與 Java 不同,因為 Java 區分基本型別(例如 booleanint)和參考型別。

數字是物件

由於數字是物件,因此它們也有方法。事實上,以下類似的算術表達式

1 + 2 * 3 / x

僅包含方法呼叫,因為它等於以下表達式,如我們在前一節中所見

1.+(2.*(3)./(x))

這也表示 +* 等是 Scala 中欄位/方法等的有效識別碼。

函式是物件

忠於所有東西都是物件,在 Scala 中,甚至函式都是物件,超越了 Java 對 lambda 表達式的支援。

與 Java 相比,函式物件和方法之間幾乎沒有差異:您可以將方法傳遞為引數、儲存在變數中,以及從其他函式傳回,所有這些都不需要特殊語法。將函式視為值進行處理的能力是稱為函式程式設計的非常有趣的程式設計範例的基石之一。

為了示範,請考慮一個計時器函式,它每秒執行一些動作。要執行的動作由呼叫者提供,作為函式值。

在以下程式中,計時器函數稱為 oncePerSecond,它取得一個呼叫回函數作為參數。此函數的類型寫成 () => Unit,這是所有不接受參數且不傳回任何有用值的函數的類型(如同先前所述,Unit 類型類似於 Java 中的 void)。

此程式的進入點透過直接傳遞 timeFlies 方法來呼叫 oncePerSecond

最後,此程式會在每秒無限列印句子 time flies like an arrow

object Timer {
  def oncePerSecond(callback: () => Unit): Unit = {
    while (true) { callback(); Thread.sleep(1000) }
  }
  def timeFlies(): Unit = {
    println("time flies like an arrow...")
  }
  def main(args: Array[String]): Unit = {
    oncePerSecond(timeFlies)
  }
}
def oncePerSecond(callback: () => Unit): Unit =
  while true do { callback(); Thread.sleep(1000) }

def timeFlies(): Unit =
  println("time flies like an arrow...")

@main def Timer: Unit =
  oncePerSecond(timeFlies)

請注意,為了列印字串,我們使用預先定義的方法 println,而不是使用 System.out 中的方法。

匿名函數

在 Scala 中,lambda 表達式稱為匿名函數。當函數非常簡短時,它們很有用,因為可能不需要為它們命名。

以下是計時器程式的修改版本,傳遞匿名函數給 oncePerSecond,而不是 timeFlies

object TimerAnonymous {
  def oncePerSecond(callback: () => Unit): Unit = {
    while (true) { callback(); Thread.sleep(1000) }
  }
  def main(args: Array[String]): Unit = {
    oncePerSecond(() =>
      println("time flies like an arrow..."))
  }
}
def oncePerSecond(callback: () => Unit): Unit =
  while true do { callback(); Thread.sleep(1000) }

@main def TimerAnonymous: Unit =
  oncePerSecond(() =>
    println("time flies like an arrow..."))

在此範例中,匿名函數的存在是由右箭頭 (=>) 揭示的,不同於 Java 的細箭頭 (->),它將函數的參數清單與其主體分開。在此範例中,參數清單是空的,因此我們在箭頭的左側放置空括號。函數的主體與上方 timeFlies 的主體相同。

類別

如上所述,Scala 是一種物件導向語言,因此它有類別的概念。(為了完整起見,應該注意的是,有些物件導向語言沒有類別的概念,但 Scala 不是其中之一。)Scala 中的類別使用接近 Java 語法之語法宣告。一個重要的差異是 Scala 中的類別可以有參數。這在以下複數定義中說明。

class Complex(real: Double, imaginary: Double) {
  def re() = real
  def im() = imaginary
}

Complex 類別接收兩個引數,它們是複數的實部和虛部。在建立 Complex 類別的實例時,必須傳遞這些引數,如下所示

new Complex(1.5, 2.3)

該類別包含兩個方法,稱為 reim,可存取這兩個部分。

class Complex(real: Double, imaginary: Double):
  def re() = real
  def im() = imaginary

Complex 類別接收兩個引數,它們是複數的實部和虛部。在建立 Complex 類別的實例時,必須傳遞這些引數,如下所示

new Complex(1.5, 2.3)

其中 new 是可選的。該類別包含兩個方法,稱為 reim,可存取這兩個部分。

應注意,這兩個方法的傳回類型並未明確給出。它將由編譯器自動推斷,編譯器會查看這些方法的右側並推論出兩個方法都傳回 Double 類型的值。

重要:如果實作變更,方法的推論結果類型可能會以微妙的方式變更,這可能會產生連鎖效應。因此,對於類別的公開成員,最好實作明確的結果類型。

對於方法中的局部值,建議推論結果類型。嘗試省略類型宣告(如果它們看起來很容易從上下文中推論),看看編譯器是否同意。經過一段時間後,程式設計師應該可以很好地掌握何時省略類型,以及何時明確指定類型。

沒有引數的方法

方法 reim 的一個小問題是,為了呼叫它們,必須在它們的名稱後加上一對空的括號,如下例所示

object ComplexNumbers {
  def main(args: Array[String]): Unit = {
    val c = new Complex(1.2, 3.4)
    println("imaginary part: " + c.im())
  }
}
@main def ComplexNumbers: Unit =
  val c = Complex(1.2, 3.4)
  println("imaginary part: " + c.im())

如果可以像存取欄位一樣存取實部和虛部,而不用加上空的括號,會更棒。這在 Scala 中完全可行,只要將它們定義為沒有參數的方法即可。這種方法與零參數的方法不同,它們在名稱後面沒有括號,無論是在定義中還是在使用中。我們的 Complex 類別可以改寫如下

class Complex(real: Double, imaginary: Double) {
  def re = real
  def im = imaginary
}
class Complex(real: Double, imaginary: Double):
  def re = real
  def im = imaginary

繼承和覆寫

Scala 中的所有類別都繼承自超類別。如果未指定超類別,如前一節中 Complex 的範例,scala.AnyRef 會被隱含使用。

可以在 Scala 中覆寫從超類別繼承的方法。但為了避免意外覆寫,必須明確指定方法使用 override 修飾詞覆寫另一個方法。舉例來說,我們的 Complex 類別可以擴充,重新定義從 Object 繼承的 toString 方法。

class Complex(real: Double, imaginary: Double) {
  def re = real
  def im = imaginary
  override def toString() =
    "" + re + (if (im >= 0) "+" else "") + im + "i"
}
class Complex(real: Double, imaginary: Double):
  def re = real
  def im = imaginary
  override def toString() =
    "" + re + (if im >= 0 then "+" else "") + im + "i"

我們可以如下呼叫覆寫的 toString 方法

object ComplexNumbers {
  def main(args: Array[String]): Unit = {
    val c = new Complex(1.2, 3.4)
    println("Overridden toString(): " + c.toString)
  }
}
@main def ComplexNumbers: Unit =
  val c = Complex(1.2, 3.4)
  println("Overridden toString(): " + c.toString)

代數資料類型和樣式比對

一種經常出現在程式中的資料結構是樹。例如,直譯器和編譯器通常會在內部將程式表示為樹;JSON 酬載是樹;而且許多種類的容器都基於樹,例如紅黑樹。

我們現在將探討如何透過一個小型計算機程式在 Scala 中表示和操作這些樹。此程式的目標是操作由加總、整數常數和變數組成的非常簡單的算術表達式。這類表達式的兩個範例是 1+2(x+x)+(7+y)

我們必須先決定如何表示這些表達式。最自然的方式是樹,其中節點是運算(這裡是加法),而葉子是值(這裡是常數或變數)。

在 Java 中,在引入記錄之前,這樣的樹會使用樹的抽象超類別來表示,並為每個節點或葉子使用一個具體的子類別。在函數式程式語言中,會使用代數資料類型 (ADT) 來達到相同的目的。

Scala 2 提供了案例類別的概念,這在兩者之間。以下是如何使用它們來定義我們範例中樹的類型

abstract class Tree
object Tree {
  case class Sum(l: Tree, r: Tree) extends Tree
  case class Var(n: String) extends Tree
  case class Const(v: Int) extends Tree
}

類別 SumVarConst 宣告為案例類別的事實表示它們在幾個方面與標準類別不同

  • new 關鍵字不是建立這些類別的實例的必要條件(也就是說,可以寫 Tree.Const(5) 而不是 new Tree.Const(5)),
  • 自動為建構函數參數定義 getter 函數(也就是說,可以取得類別 Tree.Const 的某些實例 cv 建構函數參數的值,只要寫 c.v),
  • 提供方法 equalshashCode 的預設定義,這些定義會在實例的結構上運作,而不是在它們的身分上運作,
  • 提供方法 toString 的預設定義,並以「原始碼形式」列印值(例如,表達式 x+1 的樹會列印為 Sum(Var(x),Const(1))),
  • 這些類別的實例可以透過模式配對分解,如下所示。

Scala 3 提供了列舉的概念,可以使用它就像 Java 的列舉一樣,也可以用來實作 ADT。以下是如何使用它們來定義我們範例中樹的類型

enum Tree:
  case Sum(l: Tree, r: Tree)
  case Var(n: String)
  case Const(v: Int)

列舉 SumVarConst 的情況類似於標準類別,但在幾個方面有所不同

  • 自動為建構函數參數定義 getter 函數(也就是說,可以取得情況 Tree.Const 的某些實例 cv 建構函數參數的值,只要寫 c.v),
  • 提供方法 equalshashCode 的預設定義,這些定義會在實例的結構上運作,而不是在它們的身分上運作,
  • 提供方法 toString 的預設定義,並以「原始碼形式」列印值(例如,表達式 x+1 的樹會列印為 Sum(Var(x),Const(1))),
  • 這些列舉情況的實例可以透過模式配對分解,如下所示。

現在我們已經定義了用來表示算術表達式的資料類型,我們可以開始定義操作它們的運算。我們將從一個函數開始,用於在某些環境中評估表達式。環境的目標是為變數賦予值。例如,表達式 x+1 在一個將值 5 與變數 x 關聯的環境中評估,寫成 { x -> 5 },結果為 6

因此,我們必須找到一種表示環境的方法。我們當然可以使用一些關聯資料結構,例如雜湊表,但我們也可以直接使用函數!環境實際上只是一個將值與(變數)名稱關聯的函數。上面給出的環境 { x -> 5 } 在 Scala 中可以寫成如下

type Environment = String => Int
val ev: Environment = { case "x" => 5 }

此表示法定義了一個函數,當給定字串 "x" 作為引數時,會傳回整數 5,否則會失敗並傳回例外。

在上面,我們定義了一個稱為 Environment類型別名,它比純粹的函數類型 String => Int 更具可讀性,並使未來的變更更為容易。

我們現在可以給出評估函數的定義。以下是簡要說明:Sum 的值是其兩個內部表達式評估結果的加總;Var 的值是透過在環境中查詢其內部名稱取得;Const 的值就是其本身的內部值。此說明可以透過使用樹狀值 t 上的樣式比對,精確地轉換成 Scala,如下所示

import Tree._

def eval(t: Tree, ev: Environment): Int = t match {
  case Sum(l, r) => eval(l, ev) + eval(r, ev)
  case Var(n)    => ev(n)
  case Const(v)  => v
}
import Tree.*

def eval(t: Tree, ev: Environment): Int = t match
  case Sum(l, r) => eval(l, ev) + eval(r, ev)
  case Var(n)    => ev(n)
  case Const(v)  => v

您可以了解樣式比對的精確含義,如下所示

  1. 它首先檢查樹狀結構 t 是否為 Sum,如果是,它將左子樹狀結構繫結到一個稱為 l 的新變數,並將右子樹狀結構繫結到一個稱為 r 的變數,然後繼續評估箭頭後的表達式;此表達式可以(而且確實)使用箭頭左側出現的樣式繫結的變數,即 lr
  2. 如果第一次檢查未成功,也就是說,如果樹狀結構不是 Sum,它將繼續檢查 t 是否為 Var;如果是,它將 Var 節點中包含的名稱繫結到變數 n,並繼續執行右側表達式,
  3. 如果第二次檢查也失敗,也就是說,如果 t 既不是 Sum 也不是 Var,它會檢查它是否為 Const,如果是,它會將 Const 節點中包含的值繫結到變數 v,並繼續執行右側,
  4. 最後,如果所有檢查都失敗,則會引發例外狀況,以表示模式比對表達式失敗;這只會發生在宣告更多 Tree 的子類別時。

我們看到,模式比對的基本概念是嘗試將值與一系列模式比對,並且在模式比對後,萃取並命名值的各個部分,最後評估通常會使用這些命名部分的程式碼。

與 OOP 的比較

熟悉物件導向範例的程式設計師可能會疑惑,為什麼在 Tree 的範圍之外,定義 eval 的單一函式,而不是在 Tree 中建立 eval 和抽象方法,並在 Tree 的每個子類別中提供覆寫。

我們實際上可以這樣做,這是一個需要做出的選擇,它對可延伸性具有重要的影響

  • 使用方法覆寫時,新增用於操作樹的新作業,表示需要對程式碼進行廣泛的變更,因為它需要在 Tree 的所有子類別中新增方法,然而,新增新的子類別只需要在一個地方實作作業。此設計偏好少數核心作業和許多成長中的子類別,
  • 使用模式比對時,情況相反:新增新的節點類型需要修改所有對樹執行模式比對的函式,以考量新的節點;另一方面,新增新的作業只需要在一個地方定義函式。如果資料結構具有穩定的節點組,它偏好 ADT 和模式比對設計。

新增新作業

為了進一步探討模式比對,讓我們定義算術表達式的另一個作業:符號導數。讀者可能記得關於此作業的下列規則

  1. 和的導數是導數的和,
  2. 某個變數 v 的導數為 1,如果 v 是進行導數運算的相關變數,否則為零,
  3. 常數的導數為零。

這些規則幾乎可以逐字翻譯成 Scala 程式碼,以取得下列定義

import Tree._

def derive(t: Tree, v: String): Tree = t match {
  case Sum(l, r)        => Sum(derive(l, v), derive(r, v))
  case Var(n) if v == n => Const(1)
  case _                => Const(0)
}
import Tree.*

def derive(t: Tree, v: String): Tree = t match
  case Sum(l, r)        => Sum(derive(l, v), derive(r, v))
  case Var(n) if v == n => Const(1)
  case _                => Const(0)

此函數引入了兩個與模式配對相關的新概念。首先,變數的 case 表達式有一個守衛,也就是在 if 關鍵字之後的表達式。這個守衛會防止模式配對成功,除非其表達式為真。這裡用來確保我們僅在要導出的變數名稱與導數變數 v 相同時,才傳回常數 1。這裡使用的模式配對第二個新功能是萬用字元,寫成 _,這是一個配對任何值,但不會給予名稱的模式。

我們尚未探索模式配對的全部功能,但我們將在此停止,以保持此文件簡短。我們仍然想看看上述兩個函數如何執行實際範例。為此,讓我們撰寫一個簡單的 main 函數,對表達式 (x+x)+(7+y) 執行多個運算:它首先在環境 { x -> 5, y -> 7 } 中計算其值,然後計算其相對於 x,再相對於 y 的導數。

import Tree._

object Calc {
  type Environment = String => Int
  def eval(t: Tree, ev: Environment): Int = ...
  def derive(t: Tree, v: String): Tree = ...

  def main(args: Array[String]): Unit = {
    val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
    val env: Environment = { case "x" => 5 case "y" => 7 }
    println("Expression: " + exp)
    println("Evaluation with x=5, y=7: " + eval(exp, env))
    println("Derivative relative to x:\n " + derive(exp, "x"))
    println("Derivative relative to y:\n " + derive(exp, "y"))
  }
}
import Tree.*

@main def Calc: Unit =
  val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
  val env: Environment = { case "x" => 5 case "y" => 7 }
  println("Expression: " + exp)
  println("Evaluation with x=5, y=7: " + eval(exp, env))
  println("Derivative relative to x:\n " + derive(exp, "x"))
  println("Derivative relative to y:\n " + derive(exp, "y"))

執行此程式,我們應該會取得下列輸出

Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
Evaluation with x=5, y=7: 24
Derivative relative to x:
  Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
Derivative relative to y:
  Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))

檢查輸出後,我們會看到導數的結果應在呈現給使用者之前簡化。使用模式配對定義一個基本的簡化函數是一個有趣的(但令人驚訝地棘手)問題,留給讀者練習。

特質

除了從超類別繼承程式碼之外,Scala 類別也可以從一個或多個特質匯入程式碼。

對於 Java 程式設計師來說,了解特質的最簡單方法可能是將它們視為也可以包含程式碼的介面。在 Scala 中,當類別從特質繼承時,它會實作該特質的介面,並繼承特質中包含的所有程式碼。

(請注意,自 Java 8 以來,Java 介面也可以包含程式碼,使用 default 關鍵字或作為靜態方法。)

要了解特質的用處,我們來看一個經典範例:已排序的物件。通常我們會想要比較某個類別中的物件,例如對它們進行排序。在 Java 中,可比較的物件會實作 Comparable 介面。在 Scala 中,我們可以將 Comparable 的等效項定義為特質,我們稱之為 Ord,這樣一來會比 Java 做得更好。

在比較物件時,六種不同的謂詞會很有用:小於、小於或等於、等於、不等於、大於或等於和大於。不過,要定義所有這些謂詞很繁瑣,特別是因為這六個謂詞中有四個可以用其餘兩個來表達。也就是說,給定等於和小於謂詞(例如),我們可以表達其他謂詞。在 Scala 中,所有這些觀察都可以透過下列特質宣告妥善捕捉

trait Ord {
  def < (that: Any): Boolean
  def <=(that: Any): Boolean =  (this < that) || (this == that)
  def > (that: Any): Boolean = !(this <= that)
  def >=(that: Any): Boolean = !(this < that)
}
trait Ord:
  def < (that: Any): Boolean
  def <=(that: Any): Boolean =  (this < that) || (this == that)
  def > (that: Any): Boolean = !(this <= that)
  def >=(that: Any): Boolean = !(this < that)

這個定義同時建立一個稱為 Ord 的新類型,它扮演與 Java 的 Comparable 介面相同的角色,以及三個謂詞的預設實作,而這三個謂詞是根據第四個抽象謂詞而定。等於和不等於謂詞在此處並未出現,因為它們預設存在於所有物件中。

上面使用的 Any 類型是 Scala 中所有其他類型的超類型。它可以視為 Java 的 Object 類型的更一般版本,因為它也是 IntFloat 等基本類型的超類型。

因此,要讓類別的物件可比較,就只要定義測試相等性和劣等性的謂詞,並混合上述 Ord 類別即可。舉例來說,我們來定義一個 Date 類別,用來表示格里曆中的日期。此類日期由天、月和年組成,我們會將它們全部表示為整數。因此,我們會從以下內容開始定義 Date 類別

class Date(y: Int, m: Int, d: Int) extends Ord {
  def year = y
  def month = m
  def day = d
  override def toString(): String = s"$year-$month-$day"

  // rest of implementation will go here
}
class Date(y: Int, m: Int, d: Int) extends Ord:
  def year = y
  def month = m
  def day = d
  override def toString(): String = s"$year-$month-$day"

  // rest of implementation will go here
end Date

這裡重要的部分是 extends Ord 宣告,它出現在類別名稱和參數之後。它宣告 Date 類別繼承自 Ord 特質。

接著,我們重新定義從 Object 繼承而來的 equals 方法,以便它能正確地透過比較個別欄位來比較日期。預設的 equals 實作無法使用,因為它像 Java 一樣透過身分來比較物件。我們得到下列定義

class Date(y: Int, m: Int, d: Int) extends Ord {
  // previous decls here

  override def equals(that: Any): Boolean = that match {
    case d: Date => d.day == day && d.month == month && d.year == year
    case _ => false
  }

  // rest of implementation will go here
}
class Date(y: Int, m: Int, d: Int) extends Ord:
  // previous decls here

  override def equals(that: Any): Boolean = that match
    case d: Date => d.day == day && d.month == month && d.year == year
    case _ => false

  // rest of implementation will go here
end Date

在 Java(16 版之前)中,您可能會使用 instanceof 算子,後面接一個轉型(等於呼叫 that.isInstanceOf[Date]that.asInstanceOf[Date] 在 Scala 中);在 Scala 中,使用類型模式會更慣用,如上方的範例所示,它會檢查 that 是否是 Date 的實例,並將它繫結到新的變數 d,然後在 case 的右側使用它。

最後,要定義的方法是 < 測試,如下所示。它使用另一個方法,也就是來自套件物件 scala.syserror,它會擲回一個包含給定錯誤訊息的例外狀況。

class Date(y: Int, m: Int, d: Int) extends Ord {
  // previous decls here

  def <(that: Any): Boolean = that match {
    case d: Date =>
      (year < d.year) ||
      (year == d.year && (month < d.month ||
                         (month == d.month && day < d.day)))

    case _ => sys.error("cannot compare " + that + " and a Date")
  }
}
class Date(y: Int, m: Int, d: Int) extends Ord:
  // previous decls here

  def <(that: Any): Boolean = that match
    case d: Date =>
      (year < d.year) ||
      (year == d.year && (month < d.month ||
                         (month == d.month && day < d.day)))

    case _ => sys.error("cannot compare " + that + " and a Date")
  end <
end Date

這就完成了 Date 類別的定義。這個類別的實例可以視為日期或可比較的物件。此外,它們都定義了上面提到的六個比較謂詞:equals<,因為它們直接出現在 Date 類別的定義中,而其他則因為它們從 Ord 特質繼承而來。

當然,特質在這裡所示範的情況之外也有其他用途,但深入探討它們的應用超出了本文的範圍。

泛型

在這個教學課程中,我們將探討 Scala 的最後一個特徵,即泛型。Java 程式設計師應該非常了解其語言中缺乏泛型所產生的問題,而 Java 1.5 已解決了這個缺點。

泛型是指撰寫由類型參數化的程式碼的能力。例如,撰寫連結串列函式庫的程式設計師會面臨決定要給串列元素哪種型別的問題。由於這個串列會在許多不同的環境中使用,因此無法決定元素的類型必須是,例如 Int。這將完全是武斷且過於嚴格的限制。

Java 程式設計師會使用 Object,這是所有物件的超類型。然而,這個解決方案遠非理想,因為它不適用於基本類型 (intlongfloat 等),而且這表示程式設計師必須插入許多動態類型轉換。

Scala 讓您可以定義泛型類別 (和方法) 來解決這個問題。讓我們用最簡單的容器類別的範例來探討這個問題:一個參考,它可以是空的,也可以指向某種類型的物件。

class Reference[T] {
  private var contents: T = _
  def set(value: T): Unit = { contents = value }
  def get: T = contents
}

類別 Reference 由一個稱為 T 的類型參數化,這是其元素的類型。這個類型在類別主體中用作 contents 變數的類型、set 方法的引數,以及 get 方法的傳回類型。

以上的程式碼範例介紹了 Scala 中的變數,這應該不需要進一步的說明。然而,有趣的是,給予該變數的初始值是 _,這代表一個預設值。此預設值對於數字型別是 0,對於 Boolean 型別是 false,對於 Unit 型別是 (),對於所有物件型別是 null

import compiletime.uninitialized

class Reference[T]:
  private var contents: T = uninitialized
  def set(value: T): Unit = contents = value
  def get: T = contents

類別 Reference 由一個稱為 T 的類型參數化,這是其元素的類型。這個類型在類別主體中用作 contents 變數的類型、set 方法的引數,以及 get 方法的傳回類型。

以上的程式碼範例介紹了 Scala 中的變數,這應該不需要進一步的說明。然而,有趣的是,給予該變數的初始值是 uninitialized,這代表一個預設值。此預設值對於數字型別是 0,對於 Boolean 型別是 false,對於 Unit 型別是 (),對於所有物件型別是 null

要使用這個 Reference 類別,需要為型別參數 T 指定要使用的型別,也就是儲存在儲存格中的元素型別。例如,要建立並使用儲存整數的儲存格,可以寫下以下程式碼

object IntegerReference {
  def main(args: Array[String]): Unit = {
    val cell = new Reference[Int]
    cell.set(13)
    println("Reference contains the half of " + (cell.get * 2))
  }
}
@main def IntegerReference: Unit =
  val cell = new Reference[Int]
  cell.set(13)
  println("Reference contains the half of " + (cell.get * 2))

如該範例所示,在將 get 方法傳回的值用作整數之前,不需要進行轉型。也無法將任何非整數值儲存在該特定儲存格中,因為它已宣告為儲存整數。

結論

本文快速概述了 Scala 語言,並提供了一些基本範例。有興趣的讀者可以繼續閱讀,例如閱讀包含更多說明和範例的《Scala 導覽》,並在需要時查閱《Scala 語言規格》。

此頁面的貢獻者