Skip to content

Commit 3d8b6a1

Browse files
authored
Merge pull request #12238 from MaximeKjaer/split-inline-docs
Split out docs for `scala.compiletime` from docs for `inline`
2 parents 53651f5 + 7d30fc8 commit 3d8b6a1

File tree

4 files changed

+283
-273
lines changed

4 files changed

+283
-273
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
---
2+
layout: doc-page
3+
title: "Compile-time operations"
4+
---
5+
6+
## The `scala.compiletime` Package
7+
8+
The [`scala.compiletime`](https://dotty.epfl.ch/api/scala/compiletime.html) package contains helper definitions that provide support for compile-time operations over values. They are described in the following.
9+
10+
### `constValue` and `constValueOpt`
11+
12+
`constValue` is a function that produces the constant value represented by a
13+
type.
14+
15+
```scala
16+
import scala.compiletime.constValue
17+
import scala.compiletime.ops.int.S
18+
19+
transparent inline def toIntC[N]: Int =
20+
inline constValue[N] match
21+
case 0 => 0
22+
case _: S[n1] => 1 + toIntC[n1]
23+
24+
inline val ctwo = toIntC[2]
25+
```
26+
27+
`constValueOpt` is the same as `constValue`, however returning an `Option[T]`
28+
enabling us to handle situations where a value is not present. Note that `S` is
29+
the type of the successor of some singleton type. For example the type `S[1]` is
30+
the singleton type `2`.
31+
32+
### `erasedValue`
33+
34+
So far we have seen inline methods that take terms (tuples and integers) as
35+
parameters. What if we want to base case distinctions on types instead? For
36+
instance, one would like to be able to write a function `defaultValue`, that,
37+
given a type `T`, returns optionally the default value of `T`, if it exists.
38+
We can already express this using rewrite match expressions and a simple
39+
helper function, `scala.compiletime.erasedValue`, which is defined as follows:
40+
41+
```scala
42+
def erasedValue[T]: T
43+
```
44+
45+
The `erasedValue` function _pretends_ to return a value of its type argument `T`.
46+
Calling this function will always result in a compile-time error unless the call
47+
is removed from the code while inlining.
48+
49+
Using `erasedValue`, we can then define `defaultValue` as follows:
50+
51+
```scala
52+
import scala.compiletime.erasedValue
53+
54+
inline def defaultValue[T] =
55+
inline erasedValue[T] match
56+
case _: Byte => Some(0: Byte)
57+
case _: Char => Some(0: Char)
58+
case _: Short => Some(0: Short)
59+
case _: Int => Some(0)
60+
case _: Long => Some(0L)
61+
case _: Float => Some(0.0f)
62+
case _: Double => Some(0.0d)
63+
case _: Boolean => Some(false)
64+
case _: Unit => Some(())
65+
case _ => None
66+
```
67+
68+
Then:
69+
70+
```scala
71+
val dInt: Some[Int] = defaultValue[Int]
72+
val dDouble: Some[Double] = defaultValue[Double]
73+
val dBoolean: Some[Boolean] = defaultValue[Boolean]
74+
val dAny: None.type = defaultValue[Any]
75+
```
76+
77+
As another example, consider the type-level version of `toInt` below:
78+
given a _type_ representing a Peano number,
79+
return the integer _value_ corresponding to it.
80+
Consider the definitions of numbers as in the _Inline
81+
Match_ section above. Here is how `toIntT` can be defined:
82+
83+
```scala
84+
transparent inline def toIntT[N <: Nat]: Int =
85+
inline scala.compiletime.erasedValue[N] match
86+
case _: Zero.type => 0
87+
case _: Succ[n] => toIntT[n] + 1
88+
89+
inline val two = toIntT[Succ[Succ[Zero.type]]]
90+
```
91+
92+
`erasedValue` is an `erased` method so it cannot be used and has no runtime
93+
behavior. Since `toIntT` performs static checks over the static type of `N` we
94+
can safely use it to scrutinize its return type (`S[S[Z]]` in this case).
95+
96+
### `error`
97+
98+
The `error` method is used to produce user-defined compile errors during inline expansion.
99+
It has the following signature:
100+
101+
```scala
102+
inline def error(inline msg: String): Nothing
103+
```
104+
105+
If an inline expansion results in a call `error(msgStr)` the compiler
106+
produces an error message containing the given `msgStr`.
107+
108+
```scala
109+
import scala.compiletime.{error, code}
110+
111+
inline def fail() =
112+
error("failed for a reason")
113+
114+
fail() // error: failed for a reason
115+
```
116+
117+
or
118+
119+
```scala
120+
inline def fail(p1: => Any) =
121+
error(code"failed on: $p1")
122+
123+
fail(identity("foo")) // error: failed on: identity("foo")
124+
```
125+
126+
### The `scala.compiletime.ops` package
127+
128+
The [`scala.compiletime.ops`](https://dotty.epfl.ch/api/scala/compiletime/ops.html) package contains types that provide support for
129+
primitive operations on singleton types. For example,
130+
`scala.compiletime.ops.int.*` provides support for multiplying two singleton
131+
`Int` types, and `scala.compiletime.ops.boolean.&&` for the conjunction of two
132+
`Boolean` types. When all arguments to a type in `scala.compiletime.ops` are
133+
singleton types, the compiler can evaluate the result of the operation.
134+
135+
```scala
136+
import scala.compiletime.ops.int.*
137+
import scala.compiletime.ops.boolean.*
138+
139+
val conjunction: true && true = true
140+
val multiplication: 3 * 5 = 15
141+
```
142+
143+
Many of these singleton operation types are meant to be used infix (as in [SLS §3.2.10](https://www.scala-lang.org/files/archive/spec/2.13/03-types.html#infix-types)).
144+
145+
Since type aliases have the same precedence rules as their term-level
146+
equivalents, the operations compose with the expected precedence rules:
147+
148+
```scala
149+
import scala.compiletime.ops.int.*
150+
val x: 1 + 2 * 3 = 7
151+
```
152+
153+
The operation types are located in packages named after the type of the
154+
left-hand side parameter: for instance, `scala.compiletime.ops.int.+` represents
155+
addition of two numbers, while `scala.compiletime.ops.string.+` represents string
156+
concatenation. To use both and distinguish the two types from each other, a
157+
match type can dispatch to the correct implementation:
158+
159+
```scala
160+
import scala.compiletime.ops.*
161+
162+
import scala.annotation.infix
163+
164+
type +[X <: Int | String, Y <: Int | String] = (X, Y) match
165+
case (Int, Int) => int.+[X, Y]
166+
case (String, String) => string.+[X, Y]
167+
168+
val concat: "a" + "b" = "ab"
169+
val addition: 1 + 1 = 2
170+
```
171+
172+
## Summoning Implicits Selectively
173+
174+
It is foreseen that many areas of typelevel programming can be done with rewrite
175+
methods instead of implicits. But sometimes implicits are unavoidable. The
176+
problem so far was that the Prolog-like programming style of implicit search
177+
becomes viral: Once some construct depends on implicit search it has to be
178+
written as a logic program itself. Consider for instance the problem of creating
179+
a `TreeSet[T]` or a `HashSet[T]` depending on whether `T` has an `Ordering` or
180+
not. We can create a set of implicit definitions like this:
181+
182+
```scala
183+
trait SetFor[T, S <: Set[T]]
184+
185+
class LowPriority:
186+
implicit def hashSetFor[T]: SetFor[T, HashSet[T]] = ...
187+
188+
object SetsFor extends LowPriority:
189+
implicit def treeSetFor[T: Ordering]: SetFor[T, TreeSet[T]] = ...
190+
```
191+
192+
Clearly, this is not pretty. Besides all the usual indirection of implicit
193+
search, we face the problem of rule prioritization where we have to ensure that
194+
`treeSetFor` takes priority over `hashSetFor` if the element type has an
195+
ordering. This is solved (clumsily) by putting `hashSetFor` in a superclass
196+
`LowPriority` of the object `SetsFor` where `treeSetFor` is defined. Maybe the
197+
boilerplate would still be acceptable if the crufty code could be contained.
198+
However, this is not the case. Every user of the abstraction has to be
199+
parameterized itself with a `SetFor` implicit. Considering the simple task _"I
200+
want a `TreeSet[T]` if `T` has an ordering and a `HashSet[T]` otherwise"_, this
201+
seems like a lot of ceremony.
202+
203+
There are some proposals to improve the situation in specific areas, for
204+
instance by allowing more elaborate schemes to specify priorities. But they all
205+
keep the viral nature of implicit search programs based on logic programming.
206+
207+
By contrast, the new `summonFrom` construct makes implicit search available
208+
in a functional context. To solve the problem of creating the right set, one
209+
would use it as follows:
210+
211+
```scala
212+
import scala.compiletime.summonFrom
213+
214+
inline def setFor[T]: Set[T] = summonFrom {
215+
case ord: Ordering[T] => new TreeSet[T](using ord)
216+
case _ => new HashSet[T]
217+
}
218+
```
219+
220+
A `summonFrom` call takes a pattern matching closure as argument. All patterns
221+
in the closure are type ascriptions of the form `identifier : Type`.
222+
223+
Patterns are tried in sequence. The first case with a pattern `x: T` such that an implicit value of type `T` can be summoned is chosen.
224+
225+
Alternatively, one can also use a pattern-bound given instance, which avoids the explicit using clause. For instance, `setFor` could also be formulated as follows:
226+
227+
```scala
228+
import scala.compiletime.summonFrom
229+
230+
inline def setFor[T]: Set[T] = summonFrom {
231+
case given Ordering[T] => new TreeSet[T]
232+
case _ => new HashSet[T]
233+
}
234+
```
235+
236+
`summonFrom` applications must be reduced at compile time.
237+
238+
Consequently, if we summon an `Ordering[String]` the code above will return a
239+
new instance of `TreeSet[String]`.
240+
241+
```scala
242+
summon[Ordering[String]]
243+
244+
println(setFor[String].getClass) // prints class scala.collection.immutable.TreeSet
245+
```
246+
247+
**Note** `summonFrom` applications can raise ambiguity errors. Consider the following
248+
code with two givens in scope of type `A`. The pattern match in `f` will raise
249+
an ambiguity error of `f` is applied.
250+
251+
```scala
252+
class A
253+
given a1: A = new A
254+
given a2: A = new A
255+
256+
inline def f: Any = summonFrom {
257+
case given _: A => ??? // error: ambiguous givens
258+
}
259+
```
260+
261+
## `summonInline`
262+
263+
The shorthand `summonInline` provides a simple way to write a `summon` that is delayed until the call is inlined.
264+
265+
```scala
266+
transparent inline def summonInline[T]: T = summonFrom {
267+
case t: T => t
268+
}
269+
```
270+
271+
## Reference
272+
273+
For more information about compile-time operations, see [PR #4768](https://github.com/lampepfl/dotty/pull/4768),
274+
which explains how `summonFrom`'s predecessor (implicit matches) can be used for typelevel programming and code specialization and [PR #7201](https://github.com/lampepfl/dotty/pull/7201) which explains the new `summonFrom` syntax.

0 commit comments

Comments
 (0)