|
| 1 | +--- |
| 2 | +layout: doc-page |
| 3 | +title: Numeric Literals |
| 4 | +--- |
| 5 | + |
| 6 | +In Scala 2, numeric literals were confined to the promitive numeric types `Int`, Long`, `Float`, and `Double`. Scala 3 allows to write numeric literals also for user defined types. Example: |
| 7 | +``` |
| 8 | +val x: Long = -10_000_000_000 |
| 9 | +val y: BigInt = 0x123_abc_789_def_345_678_901 |
| 10 | +val z: BigDecimal = 110_222_799_799.99 |
| 11 | +
|
| 12 | +(y: BigInt) match { |
| 13 | + case 123_456_789_012_345_678_901 => |
| 14 | +} |
| 15 | +``` |
| 16 | +The syntax of numeric literals is the same as before, except there are no pre-set limits |
| 17 | +how large they can be. |
| 18 | + |
| 19 | +### Meaning of Numeric Literals |
| 20 | + |
| 21 | +The meaning of a numeric literal is determined as follows: |
| 22 | + |
| 23 | + - If the literal ends with `l` or `L`, it is a `Long` integer (and must fit |
| 24 | + in its legal range). |
| 25 | + - If the literal ends with `f` or `F`, it is a single precision floating point number of type `Float`. |
| 26 | + - If the literal ends with `d` or `D`, it is a double precision floating point number of type `Double`. |
| 27 | + |
| 28 | +In each of these cases the conversion to a number is exactly as in Scala 2 or in Java. If a numeric literal does _not_ end in one of these suffixes, its meaning is determined by the expected type: |
| 29 | + |
| 30 | + 1. If the expected type is `Int`, `Long`, `Float`, or `Double`, the literal is |
| 31 | + treated as a standard literal of that type. |
| 32 | + 2. If the expected type is a fully defined type `T` that has a given instance of type |
| 33 | + `scala.util.FromDigits[T]`, the literal is converted to a value of type `T` by passing it as an argument to |
| 34 | + the `fromDigits` method of that instance (more details below). |
| 35 | + 3. Otherwise, the literal is treated as a `Double` literal (if it has a decimal point or an |
| 36 | + exponent), or as an `Int` literal (if not). (This last possibility is again as in Scala 2 or Java.) |
| 37 | + |
| 38 | +With these rules, the definition |
| 39 | +```scala |
| 40 | +val x: Long = -10_000_000_000 |
| 41 | +``` |
| 42 | +is legal by rule (1), since the expected type is `Long`. The definitions |
| 43 | +```scala |
| 44 | +val y: BigInt = 0x123_abc_789_def_345_678_901 |
| 45 | +val z: BigDecimal = 111222333444.55 |
| 46 | +``` |
| 47 | +are legal by rule (2), since both `BigInt` and `BigDecimal` have `FromDigits` instances |
| 48 | +(which implement the `FromDigits` subclasses `FromDigits.WithRadix` and `FromDigits.Decimal`, respectively). |
| 49 | +On the other hand, |
| 50 | +```scala |
| 51 | +val x = -10_000_000_000 |
| 52 | +``` |
| 53 | +gives a type error, since without an expected type `-10_000_000_000` is treated by rule (3) as an `Int` literal, but it is too large for that type. |
| 54 | + |
| 55 | +### The FromDigits Class |
| 56 | + |
| 57 | +To allow numeric literals, a type simply has to define a given instance of the |
| 58 | +`scala.util.FromDigits` typeclass, or one of its subclasses. `FromDigits` is defined |
| 59 | +as follows: |
| 60 | +```scala |
| 61 | +trait FromDigits[T] { |
| 62 | + def fromDigits(digits: String): T |
| 63 | +} |
| 64 | +``` |
| 65 | +Implementations of the `fromDigits` convert strings of digits to the values of the |
| 66 | +implementation type `T`. |
| 67 | +The `digits` string consists of digits between `0` and `9`, possibly preceded by a |
| 68 | +sign ("+" or "-"). Number separator characters `_` are filtered out before |
| 69 | +the string is passed to `fromDigits`. |
| 70 | + |
| 71 | +The companion object `FromDigits` also defines subclasses of `FromDigits` for |
| 72 | +whole numbers with a given radix, for numbers with a decimal point, and for |
| 73 | +numbers that can have both a decimal point and an exponent: |
| 74 | +```scala |
| 75 | +object FromDigits { |
| 76 | + |
| 77 | + /** A subclass of `FromDigits` that also allows to convert whole number literals |
| 78 | + * with a radix other than 10 |
| 79 | + */ |
| 80 | + trait WithRadix[T] extends FromDigits[T] { |
| 81 | + def fromDigits(digits: String): T = fromDigits(digits, 10) |
| 82 | + def fromDigits(digits: String, radix: Int): T |
| 83 | + } |
| 84 | + |
| 85 | + /** A subclass of `FromDigits` that also allows to convert number |
| 86 | + * literals containing a decimal point ".". |
| 87 | + */ |
| 88 | + trait Decimal[T] extends FromDigits[T] |
| 89 | + |
| 90 | + /** A subclass of `FromDigits`that allows also to convert number |
| 91 | + * literals containing a decimal point "." or an |
| 92 | + * exponent `('e' | 'E')['+' | '-']digit digit*`. |
| 93 | + */ |
| 94 | + trait Floating[T] extends Decimal[T] |
| 95 | + ... |
| 96 | +} |
| 97 | +``` |
| 98 | +A user-defined number type can implement one of those, which signals to the compiler |
| 99 | +that hexadecimal numbers, decimal points, or exponents are also accepted in literals |
| 100 | +for this type. |
| 101 | + |
| 102 | +### Error Handling |
| 103 | + |
| 104 | +`FromDigits` implementations can signal errors by throwing exceptions of some subtype |
| 105 | +of `FromDigitsException`. `FromDigitsException` is defined with three subclasses in the |
| 106 | +`FromDigits` object as follows: |
| 107 | +```scala |
| 108 | + abstract class FromDigitsException(msg: String) extends NumberFormatException(msg) |
| 109 | + |
| 110 | + class NumberTooLarge (msg: String = "number too large") extends FromDigitsException(msg) |
| 111 | + class NumberTooSmall (msg: String = "number too small") extends FromDigitsException(msg) |
| 112 | + class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg) |
| 113 | +``` |
| 114 | + |
| 115 | +### Example |
| 116 | + |
| 117 | +As a fully worked out example, here is an implementation of a new numeric class, `BigFloat`, that accepts numeric literals. `BigFloat` is defined in terms of a `BigInt` mantissa and an `Int` exponent: |
| 118 | +```scala |
| 119 | +case class BigFloat(mantissa: BigInt, exponent: Int) { |
| 120 | + override def toString = s"${mantissa}e${exponent}" |
| 121 | +} |
| 122 | +``` |
| 123 | +`BigFloat` literals can have a decimal point as well as an exponent. E.g. the following expression |
| 124 | +should produce the `BigFloat` number `BigFloat(-123, 997)`: |
| 125 | +```scala |
| 126 | +-0.123E+1000: BigFloat |
| 127 | +``` |
| 128 | +The companion object of `BigFloat` defines an `apply` constructor method to construct a `BigFloat` |
| 129 | +from a `digits` string. Here is a possible implementation: |
| 130 | +```scala |
| 131 | +object BigFloat extends App { |
| 132 | + import scala.util.FromDigits |
| 133 | + |
| 134 | + def apply(digits: String): BigFloat = { |
| 135 | + val (mantissaDigits, givenExponent) = digits.toUpperCase.split('E') match { |
| 136 | + case Array(mantissaDigits, edigits) => |
| 137 | + val expo = |
| 138 | + try FromDigits.intFromDigits(edigits) |
| 139 | + catch { |
| 140 | + case ex: FromDigits.NumberTooLarge => |
| 141 | + throw FromDigits.NumberTooLarge(s"exponent too large: $edigits") |
| 142 | + } |
| 143 | + (mantissaDigits, expo) |
| 144 | + case Array(mantissaDigits) => |
| 145 | + (mantissaDigits, 0) |
| 146 | + } |
| 147 | + val (intPart, exponent) = mantissaDigits.split('.') match { |
| 148 | + case Array(intPart, decimalPart) => |
| 149 | + (intPart ++ decimalPart, givenExponent - decimalPart.length) |
| 150 | + case Array(intPart) => |
| 151 | + (intPart, givenExponent) |
| 152 | + } |
| 153 | + BigFloat(BigInt(intPart), exponent) |
| 154 | + } |
| 155 | +``` |
| 156 | +To accept `BigFloat` literals, all that's needed in addition is a given instance of type |
| 157 | +`FromDigits.Floating[BigFloat]`: |
| 158 | +```scala |
| 159 | + given FromDigits as FromDigits.Floating[BigFloat] { |
| 160 | + def fromDigits(digits: String) = apply(digits) |
| 161 | + } |
| 162 | +} // end BigFloat |
| 163 | +``` |
| 164 | +Note that the `apply` method does not check the format of the `digits` argument. It is |
| 165 | +assumed that only valid arguments are passed. For calls coming from the compiler |
| 166 | +that assumption is valid, since the compiler will first check whether a numeric |
| 167 | +literal has the correct format before it gets passed on to a conversion method. |
| 168 | + |
| 169 | +### Compile-Time Errors |
| 170 | + |
| 171 | +With the setup of the previous section, a literal like |
| 172 | +```scala |
| 173 | +1e10_0000_000_000: BigFloat |
| 174 | +``` |
| 175 | +would be expanded by the compiler to |
| 176 | +```scala |
| 177 | +BigFloat.FromDigits.fromDigits("1e10_0000_000_000") |
| 178 | +``` |
| 179 | +Evaluating this expression throws a `NumberTooLarge` exception at run time. We would like it to |
| 180 | +produce a compile-time error instead. We can achieve this by tweaking the `BigFloat` class |
| 181 | +with a small dose of meta-programming. The idea is to turn the `fromDigits` method |
| 182 | +of the into a macro, i.e. make it an inline method with a splice as right hand side. |
| 183 | +To do this, replace the `FromDigits` instance in the `BigFloat` object by the following two definitions: |
| 184 | +```scala |
| 185 | +object BigFloat { |
| 186 | + ... |
| 187 | + |
| 188 | + class FromDigits extends FromDigits.Floating[BigFloat] { |
| 189 | + def fromDigits(digits: String) = apply(digits) |
| 190 | + } |
| 191 | + |
| 192 | + given as FromDigits { |
| 193 | + override inline def fromDigits(digits: String) = ${ |
| 194 | + fromDigitsImpl('digits) |
| 195 | + } |
| 196 | + } |
| 197 | +``` |
| 198 | +Note that an inline method cannot directly fill in for an abstract method, since it produces |
| 199 | +no code that can be executed at runtime. That's why we define an intermediary class |
| 200 | +`FromDigits` that contains a fallback implementation which is then overridden by the inline |
| 201 | +method in the `FromDigits` given instance. That method is defined in terms of a macro |
| 202 | +implementation method `fromDigitsImpl`. Here is its definition: |
| 203 | +```scala |
| 204 | + private def fromDigitsImpl(digits: Expr[String]) given (ctx: QuoteContext): Expr[BigFloat] = |
| 205 | + digits match { |
| 206 | + case Const(ds) => |
| 207 | + try { |
| 208 | + val BigFloat(m, e) = apply(ds) |
| 209 | + '{BigFloat(${m.toExpr}, ${e.toExpr})} |
| 210 | + } |
| 211 | + catch { |
| 212 | + case ex: FromDigits.FromDigitsException => |
| 213 | + ctx.error(ex.getMessage) |
| 214 | + '{BigFloat(0, 0)} |
| 215 | + } |
| 216 | + case digits => |
| 217 | + '{apply($digits)} |
| 218 | + } |
| 219 | +} |
| 220 | +``` |
| 221 | +The macro implementation takes an argument of type `Expr[String]` and yields |
| 222 | +a result of type `Expr[BigFloat]`. It tests whether its argument is a constant |
| 223 | +string. If that's the case, it converts the string using the `apply` method |
| 224 | +and lifts the resulting `BigFloat` back to `Expr` level. For non-constant |
| 225 | +strings `fromDigitsImpl(digits)` is simply `apply(digits)`, i.e. everything is |
| 226 | +evaluated at runtime in this case. |
| 227 | + |
| 228 | +The interesting part is the `catch` part of the case where `digits` is constant. |
| 229 | +If the `apply` method throws a `FromDigitsException`, the exception's message is issued as a compile time error |
| 230 | +in the `ctx.error(ex.getMessage)` call. |
| 231 | + |
| 232 | +With this new implementation, a definition like |
| 233 | +```scala |
| 234 | + val x: BigFloat = 1234.45e3333333333 |
| 235 | +``` |
| 236 | +would give a compile time error message: |
| 237 | +```scala |
| 238 | +3 | val x: BigFloat = 1234.45e3333333333 |
| 239 | + | ^^^^^^^^^^^^^^^^^^ |
| 240 | + | exponent too large: 3333333333 |
| 241 | +``` |
| 242 | + |
| 243 | + |
| 244 | + |
| 245 | + |
0 commit comments