数值字面量
此功能尚未成为 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
会产生类型错误,因为在没有预期类型的情况下,-10_000_000_000根据规则 (3) 被视为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