Skip to content

Commit 81e3678

Browse files
committed
Add docs
1 parent 34bd48f commit 81e3678

File tree

5 files changed

+292
-11
lines changed

5 files changed

+292
-11
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+

docs/sidebar.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ sidebar:
103103
url: docs/reference/other-new-features/threadUnsafe-annotation.html
104104
- title: Other Changed Features
105105
subsection:
106+
- title: Numeric Literals
107+
url: docs/reference/changed-features/numeric-literals.html
106108
- title: Structural Types
107109
url: docs/reference/changed-features/structural-types.html
108110
- title: Operators

library/src/scala/util/FromDigits.scala

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,62 @@ import quoted.matching._
55
import internal.Chars.digit2int
66
import annotation.internal.sharable
77

8+
/** A typeclass for types that admit numeric literals.
9+
*/
810
trait FromDigits[T] {
9-
/** Can throw java.lang.IllegalArgumentException */
11+
12+
/** Convert `digits` string to value of type `T`
13+
* `digits` can contain
14+
* - sign `+` or `-`
15+
* - sequence of digits between 0 and 9
16+
*
17+
* @throws MalformedNumber if digit string is not legal for the given type
18+
* @throws NumberTooLarge if value of result does not fit into `T`'s range
19+
* @throws NumberTooSmall in case of numeric underflow (e.g. a non-zero
20+
* floating point literal that produces a zero value)
21+
*/
1022
def fromDigits(digits: String): T
1123
}
1224

1325
object FromDigits {
1426

27+
/** A subclass of `FromDigits` that also allows to convert whole number literals
28+
* with a radix other than 10
29+
*/
1530
trait WithRadix[T] extends FromDigits[T] {
1631
def fromDigits(digits: String): T = fromDigits(digits, 10)
32+
33+
/** Convert digits string with given radix to numberof type `T`.
34+
* E.g. if radix is 16, digits `a..f` and `A..F` are also allowed.
35+
*/
1736
def fromDigits(digits: String, radix: Int): T
1837
}
1938

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

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

50+
/** The base type for exceptions that can be thrown from
51+
* `fromDigits` conversions
52+
*/
2453
abstract class FromDigitsException(msg: String) extends NumberFormatException(msg)
2554

55+
/** Thrown if value of result does not fit into result type's range */
2656
class NumberTooLarge(msg: String = "number too large") extends FromDigitsException(msg)
57+
58+
/** Thrown in case of numeric underflow (e.g. a non-zero
59+
* floating point literal that produces a zero value)
60+
*/
2761
class NumberTooSmall(msg: String = "number too small") extends FromDigitsException(msg)
62+
63+
/** Thrown if digit string is not legal for the given type */
2864
class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg)
2965

3066
/** Convert digits and radix to integer value (either int or Long)
@@ -39,8 +75,8 @@ object FromDigits {
3975
var i = 0
4076
var negated = false
4177
val len = digits.length
42-
if (0 < len && digits(0) == '-') {
43-
negated = true
78+
if (0 < len && (digits(0) == '-' || digits(0) == '+')) {
79+
negated = digits(0) == '-'
4480
i += 1
4581
}
4682
if (i == len) throw MalformedNumber()
@@ -118,14 +154,6 @@ object FromDigits {
118154
x
119155
}
120156

121-
given LongFromDigits as FromDigits.WithRadix[Long] {
122-
def fromDigits(digits: String, radix: Int): Long = longFromDigits(digits, radix)
123-
}
124-
125-
given FloatFromDigits as FromDigits.Floating[Float] {
126-
def fromDigits(digits: String): Float = floatFromDigits(digits)
127-
}
128-
129157
given BigIntFromDigits as FromDigits.WithRadix[BigInt] {
130158
def fromDigits(digits: String, radix: Int): BigInt = BigInt(digits, radix)
131159
}

tests/run/BigFloat.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
123445e-2
2+
123445678e-5
3+
123445e1
4+
-123e997
5+
too large

tests/run/BigFloat/Test_2.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ object Test extends App {
55
println(BigFloat("1234.45"))
66
println(BigFloat("1234.45" ++ "678"))
77
println(BigFloat("1234.45e3"))
8+
println(BigFloat("-0.123E+1000"))
89
try println(BigFloat("1234.45e3333333333"))
910
catch {
1011
case ex: FromDigits.FromDigitsException => println("too large")

0 commit comments

Comments
 (0)