數字文字
此功能尚未成為 Scala 3 語言定義的一部分。它可透過語言匯入來使用
import scala.language.experimental.genericNumberLiterals
在 Scala 2 中,數字文字僅限於基本數字類型 Int
、Long
、Float
和 Double
。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 =>
數字文字的語法與之前相同,但沒有預設大小限制。
數字文字的意義
數字文字的意義由下列方式決定
- 如果文字以
l
或L
結尾,則為Long
整數(且必須符合其合法範圍)。 - 如果文字以
f
或F
結尾,則為類型為Float
的單精度浮點數。 - 如果文字以
d
或D
結尾,則為類型為Double
的雙精度浮點數。
在這些情況中,轉換為數字的方式與 Scala 2 或 Java 中完全相同。如果數字文字沒有以這些字尾結尾,則其意義由預期類型決定
- 如果預期類型為
Int
、Long
、Float
或Double
,則文字會被視為該類型的標準文字。 - 如果預期的類型是一個完全定義的類型
T
,它有一個給定的類型實例scala.util.FromDigits[T]
,字面值會透過將其傳遞為該實例的fromDigits
方法的參數來轉換為類型T
的值(詳情如下)。 - 否則,字面值會被視為
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) 是合法的,因為 BigInt
和 BigDecimal
都具有 FromDigits
實例(分別實作 FromDigits
子類別 FromDigits.WithRadix
和 FromDigits.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
字串包含介於 0
和 9
之間的數字,前面可能會加上符號(「+」或「-」)。數字分隔字元 _
會在字串傳遞給 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
的某些子類別的例外狀況來發出錯誤訊號。FromDigitsException
在 FromDigits
物件中定義了三個子類別,如下所示
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)
範例
以下是一個完整實作範例,是一個接受數字文字的新數字類別 BigFloat
。BigFloat
是根據 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