在 GitHub 上編輯此頁面

數字文字

此功能尚未成為 Scala 3 語言定義的一部分。它可透過語言匯入來使用

import scala.language.experimental.genericNumberLiterals

在 Scala 2 中,數字文字僅限於基本數字類型 IntLongFloatDouble。Scala 3 允許為使用者定義的類型撰寫數字文字。範例

val x: Long = -10_000_000_000
val y: BigInt = 0x123_abc_789_def_345_678_901
val z: BigDecimal = 110_222_799_799.99

(y: BigInt) match
  case 123_456_789_012_345_678_901 =>

數字文字的語法與之前相同,但沒有預設大小限制。

數字文字的意義

數字文字的意義由下列方式決定

  • 如果文字以 lL 結尾,則為 Long 整數(且必須符合其合法範圍)。
  • 如果文字以 fF 結尾,則為類型為 Float 的單精度浮點數。
  • 如果文字以 dD 結尾,則為類型為 Double 的雙精度浮點數。

在這些情況中,轉換為數字的方式與 Scala 2 或 Java 中完全相同。如果數字文字沒有以這些字尾結尾,則其意義由預期類型決定

  1. 如果預期類型為 IntLongFloatDouble,則文字會被視為該類型的標準文字。
  2. 如果預期的類型是一個完全定義的類型 T,它有一個給定的類型實例 scala.util.FromDigits[T],字面值會透過將其傳遞為該實例的 fromDigits 方法的參數來轉換為類型 T 的值(詳情如下)。
  3. 否則,字面值會被視為 Double 字面值(如果它有小數點或指數),或 Int 字面值(如果沒有)。(後一種可能性再次與 Scala 2 或 Java 相同。)

根據這些規則,定義

val x: Long = -10_000_000_000

根據規則 (1) 是合法的,因為預期的類型是 Long。定義

val y: BigInt = 0x123_abc_789_def_345_678_901
val z: BigDecimal = 111222333444.55

根據規則 (2) 是合法的,因為 BigIntBigDecimal 都具有 FromDigits 實例(分別實作 FromDigits 子類別 FromDigits.WithRadixFromDigits.Decimal)。另一方面,

val x = -10_000_000_000

會產生類型錯誤,因為在沒有預期類型的情況下,根據規則 (3),-10_000_000_000 會被視為 Int 字面值,但它對於該類型來說太大。

FromDigits 特質

若要允許數字字面值,類型只需定義 scala.util.FromDigits 類型類別或其子類別的 given 實例。FromDigits 定義如下

trait FromDigits[T]:
  def fromDigits(digits: String): T

fromDigits 的實作會將數字字串轉換為實作類型 T 的值。digits 字串包含介於 09 之間的數字,前面可能會加上符號(「+」或「-」)。數字分隔字元 _ 會在字串傳遞給 fromDigits 之前過濾掉。

伴隨物件 FromDigits 也會定義 FromDigits 的子類別,用於具有特定基數的整數、具有小數點的數字,以及可以同時具有小數點和指數的數字

object FromDigits:

  /** A subclass of `FromDigits` that also allows to convert whole
   *  number literals with a radix other than 10
   */
  trait WithRadix[T] extends FromDigits[T]:
    def fromDigits(digits: String): T = fromDigits(digits, 10)
    def fromDigits(digits: String, radix: Int): T

  /** A subclass of `FromDigits` that also allows to convert number
   *  literals containing a decimal point ".".
   */
  trait Decimal[T] extends FromDigits[T]

  /** A subclass of `FromDigits`that allows also to convert number
   *  literals containing a decimal point "." or an
   *  exponent `('e' | 'E')['+' | '-']digit digit*`.
   */
  trait Floating[T] extends Decimal[T]

使用者定義的數字類型可以實作其中一種,這會向編譯器發出訊號,表示十六進位數字、小數點或指數也接受此類型的文字。

錯誤處理

FromDigits 實作可以透過擲出 FromDigitsException 的某些子類別的例外狀況來發出錯誤訊號。FromDigitsExceptionFromDigits 物件中定義了三個子類別,如下所示

abstract class FromDigitsException(msg: String) extends NumberFormatException(msg)

class NumberTooLarge (msg: String = "number too large")         extends FromDigitsException(msg)
class NumberTooSmall (msg: String = "number too small")         extends FromDigitsException(msg)
class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg)

範例

以下是一個完整實作範例,是一個接受數字文字的新數字類別 BigFloatBigFloat 是根據 BigInt 尾數和 Int 指數定義的

case class BigFloat(mantissa: BigInt, exponent: Int):
  override def toString = s"${mantissa}e${exponent}"

BigFloat 文字可以有小數點和指數。例如,下列表達式應產生 BigFloat 數字 BigFloat(-123, 997)

-0.123E+1000: BigFloat

BigFloat 的伴生物件定義了一個 apply 建構函數方法,用於從 digits 字串建構一個 BigFloat。以下是一個可能的實作

object BigFloat:
  import scala.util.FromDigits

  def apply(digits: String): BigFloat =
    val (mantissaDigits, givenExponent) =
      digits.toUpperCase.split('E') match
        case Array(mantissaDigits, edigits) =>
          val expo =
            try FromDigits.intFromDigits(edigits)
            catch case ex: FromDigits.NumberTooLarge =>
              throw FromDigits.NumberTooLarge(s"exponent too large: $edigits")
          (mantissaDigits, expo)
        case Array(mantissaDigits) =>
          (mantissaDigits, 0)
    val (intPart, exponent) =
      mantissaDigits.split('.') match
        case Array(intPart, decimalPart) =>
          (intPart ++ decimalPart, givenExponent - decimalPart.length)
        case Array(intPart) =>
          (intPart, givenExponent)
    BigFloat(BigInt(intPart), exponent)

若要接受 BigFloat 文字,除了 FromDigits.Floating[BigFloat] 類型的 given 實例之外,不需要其他任何東西

given FromDigits: FromDigits.Floating[BigFloat] with
    def fromDigits(digits: String) = apply(digits)
end BigFloat

請注意,apply 方法不會檢查 digits 參數的格式。假設只傳遞有效的參數。對於來自編譯器的呼叫,這個假設是有效的,因為編譯器會在將數字文字傳遞給轉換方法之前,先檢查數字文字是否具有正確的格式。

編譯時期錯誤

透過前一節的設定,編譯器會將像這樣的文字

1e10_0000_000_000: BigFloat

擴充為

BigFloat.FromDigits.fromDigits("1e100000000000")

評估此表達式會在執行時期擲出 NumberTooLarge 例外。我們希望它會產生編譯時期錯誤。我們可以透過加入少量的元程式設計來調整 BigFloat 類別來達成這個目的。這個想法是將 fromDigits 方法轉換為巨集,也就是說,讓它成為一個內聯方法,並在右側使用串接。為此,請用下列兩個定義取代 BigFloat 物件中的 FromDigits 實例

object BigFloat:
  ...

  class FromDigits extends FromDigits.Floating[BigFloat]:
    def fromDigits(digits: String) = apply(digits)

  given FromDigits with
    override inline def fromDigits(digits: String) = ${
      fromDigitsImpl('digits)
    }

請注意,內聯方法無法直接填入抽象方法,因為它不會產生可以在執行期間執行的程式碼。這就是為什麼我們定義一個中介類別 FromDigits,其中包含一個備用實作,然後由 FromDigits 給定實例中的內聯方法覆寫。該方法是根據巨集實作方法 fromDigitsImpl 定義的。以下是其定義

private def fromDigitsImpl(digits: Expr[String])(using ctx: Quotes): Expr[BigFloat] =
    digits.value match
      case Some(ds) =>
        try
          val BigFloat(m, e) = apply(ds)
          '{BigFloat(${Expr(m)}, ${Expr(e)})}
        catch case ex: FromDigits.FromDigitsException =>
          ctx.error(ex.getMessage)
          '{BigFloat(0, 0)}
      case None =>
        '{apply($digits)}
end BigFloat

巨集實作會採用 Expr[String] 類型的引數,並產生 Expr[BigFloat] 類型的結果。它會測試其引數是否為常數字串。如果是,它會使用 apply 方法轉換字串,並將產生的 BigFloat 提升回 Expr 層級。對於非常數字串,fromDigitsImpl(digits) 僅為 apply(digits),即在這種情況下,一切都在執行期間評估。

有趣的部分是 digits 為常數時的情況的 catch 部分。如果 apply 方法擲回 FromDigitsException,則會在 ctx.error(ex.getMessage) 呼叫中將例外狀況訊息發布為編譯時期錯誤。

有了這個新的實作,像這樣的定義

val x: BigFloat = 1234.45e3333333333

會產生編譯時期錯誤訊息

3 |  val x: BigFloat = 1234.45e3333333333
  |                    ^^^^^^^^^^^^^^^^^^
  |                    exponent too large: 3333333333