|
| 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