Skip to content

Commit 1644fd3

Browse files
committed
Update doc page to match SIP document more closely
1 parent 176b118 commit 1644fd3

File tree

1 file changed

+106
-25
lines changed
  • docs/_docs/reference/experimental

1 file changed

+106
-25
lines changed

docs/_docs/reference/experimental/into.md

+106-25
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
layout: doc-page
3-
title: "The `into` Type"
4-
redirectFrom: /docs/reference/other-new-features/into-modifier.html
5-
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/into-modifier.html
3+
title: The `into` Type and Modifier
4+
redirectFrom: /docs/reference/other-new-features/into.html
5+
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/into.html
66
---
77

88
This feature is not yet part of the Scala 3 language definition. It can be made available by a language import:
@@ -11,6 +11,21 @@ This feature is not yet part of the Scala 3 language definition. It can be made
1111
import scala.language.experimental.into
1212
```
1313

14+
15+
## Summary
16+
17+
Scala 3 offers two alternative schemes to allow implicit conversions using Scala-3's `Conversion`
18+
class without requiring a language import.
19+
20+
The first scheme is
21+
to have a special type `into[T]` which serves as a marker that conversions into that type are allowed. These types are typically used in parameters of methods that are designed to work with implicit conversions of their arguments. This allows fine-grained control over where implicit conversions should be allowed. We call this scheme "_into as a type constructor_".
22+
23+
The second scheme allows `into` as a soft modifier on traits and classes. If a trait or class is declared with this modifier, conversions to that type are allowed. The second scheme requires that one has control over the conversion target types so that an `into` can be added to their declaration. It is appropriate where there are a few designated types that are meant to be conversion targets. If that's the case, migration from Scala 2 to Scala 3
24+
becomes easier since no function signatures need to be rewritten. We call this scheme "_into as a modifier_".
25+
26+
27+
## Motivation
28+
1429
Scala 3's implicit conversions of the `scala.Conversion` class require a language import
1530
```
1631
import scala.language.implicitConversions
@@ -29,7 +44,9 @@ val res0: List[Int] = List(0, 1, 2, 3)
2944
```
3045
The input line `xs ++ ys` makes use of an implicit conversion from `Array[Int]` to `IterableOnce[Int]`. This conversion is defined in the standard library as an `implicit def`. Once the standard library is rewritten with Scala 3 conversions, this will require a language import at the use site, which is clearly unacceptable. It is possible to avoid the need for implicit conversions using method overloading or type classes, but this often leads to longer and more complicated code, and neither of these alternatives work for vararg parameters.
3146

32-
This is where the `into` type alias comes in. Here is a signature of a `++` method on `List[A]` that uses it:
47+
## First Scheme: `into` as a Type Constructor
48+
49+
This is where the `into` type constructor comes in. Here is a signature of a `++` method on `List[A]` that uses it:
3350

3451
```scala
3552
def ++ (elems: into[IterableOnce[A]]): List[A]
@@ -44,7 +61,7 @@ Types of the form `into[T]` are treated specially during type checking. If the e
4461

4562
Note: Unlike other types, `into` starts with a lower-case letter. This emphasizes the fact that `into` is treated specially by the compiler, by making `into` look more like a keyword than a regular type.
4663

47-
**Example:**
64+
### Example 1
4865

4966
```scala
5067
given Conversion[Array[Int], IterableOnce[Int]] = wrapIntArray
@@ -54,7 +71,30 @@ xs ++ ys
5471
```
5572
This inserts the given conversion on the `ys` argument in `xs ++ ys`. It typechecks without a feature warning since the formal parameter of `++` is of type `into[IterableOnce]`, which is also the expected type of `ys`.
5673

57-
## `into` in Function Results
74+
### Example 2
75+
76+
Consider a simple expression AST type:
77+
```scala
78+
enum Expr:
79+
case Neg(e: Expr)
80+
case Add(e1: Expr, e2: Expr)
81+
case Const(n: Int)
82+
import Expr.*
83+
```
84+
Say we'd like to build `Expr` trees without explicit `Const` wrapping, as in `Add(1, Neg(2))`. The usual way to achieve this is with an implicit conversion from `Int` to `Const`:
85+
```scala
86+
given Conversion[Int, Const] = Const(_)
87+
```
88+
Normally, that would require a language import in all source modules that construct `Expr` trees. We can avoid this requirement on user code by declaring `Neg` and `Add` with `into` parameters:
89+
```scala
90+
enum Expr:
91+
case Neg(e: into[Expr])
92+
case Add(e1: into[Expr], e2: into[Expr])
93+
case Const(n: Int)
94+
```
95+
This would allow conversions from `Int` to `Const` when constructing trees but not elsewhere.
96+
97+
### `into` in Function Results
5898

5999
`into` allows conversions everywhere it appears as expected type, including in the results of function arguments. For instance, consider the new proposed signature of the `flatMap` method on `List[A]`:
60100

@@ -70,7 +110,7 @@ val res2: List[Char] = List(1, 2, 2, 3, 3, 3)
70110
```
71111
Here, the conversion from `String` to `Iterable[Char]` is applied on the results of `flatMap`'s function argument when it is applied to the elements of `xs`.
72112

73-
## Vararg arguments
113+
### Vararg arguments
74114

75115
When applied to a vararg parameter, `into` allows a conversion on each argument value individually. For example, consider a method `concatAll` that concatenates a variable
76116
number of `IterableOnce[Char]` arguments, and also allows implicit conversions into `IterableOnce[Char]`:
@@ -86,7 +126,7 @@ concatAll(List('a'), "bc", Array('d', 'e'))
86126
would apply two _different_ implicit conversions: the conversion from `String` to `Iterable[Char]` gets applied to the second argument and the conversion from `Array[Char]` to `Iterable[Char]` gets applied to the third argument.
87127

88128

89-
## Unwrapping `into`
129+
### Unwrapping `into`
90130

91131
Since `into[T]` is an opaque type, its run-time representation is just `T`.
92132
At compile time, the type `into[T]` is a known supertype of the type `T`. So if `t: T`, then
@@ -107,7 +147,7 @@ However, the next section shows that unwrapping with `.underlying` is not needed
107147

108148

109149

110-
## Dropping `into` for Parameters in Method Bodies
150+
### Dropping `into` for Parameters in Method Bodies
111151

112152
The typical use cases for `into` wrappers are for parameters. Here, they specify that the
113153
corresponding arguments can be converted to the formal parameter types. On the other hand, inside a method, a parameter type can be assumed to be of the underlying type since the conversion already took place when the enclosing method was called. This is reflected in the type system which erases `into` wrappers in the local types of parameters
@@ -123,36 +163,42 @@ Inside the `++` method, the `elems` parameter is of type `IterableOnce[A]`, not
123163

124164
Specifically, we erase all `into` wrappers in the local types of parameter types that appear in covariant or invariant position. Contravariant `into` wrappers are kept since these typically are on the parameters of function arguments.
125165

166+
### Into Constructors in Type Aliases
126167

127-
## Into in Aliases
128-
129-
Since `into` is a regular type constructor, it can be used anywhere, including in type aliases and type parameters. This gives a lot of flexibility to enable implicit conversions for user-visible types. For instance, the Laminar framework
130-
defines a type `Modifier` that is commonly used as a parameter type of user-defined methods and that should support implicit conversions into it. Patterns like this can be supported by defining a type alias such as
168+
Since `into` is a regular type constructor, it can be used anywhere, including in type aliases and type parameters. For instance, in the Scala standard library we could define
131169
```scala
132-
type Modifier = into[ModifierClass]
170+
type ToIterator[T] = into[IterableOnce[T]]
133171
```
134-
The into-erasure for function parameters also works in aliased types. So a function defining parameters of `Modifier` type can use them internally as if they were from the underlying `ModifierClass`.
172+
and then `++`, `flatMap` and other functions could use this alias in their parameter types. The effect would be the same as when `into` is written out explicitly.
135173

136-
## Alternative: `into` as a Modifier
174+
## Second Scheme: `into` as a Modifier
137175

138176
The `into` scheme discussed so far strikes a nice balance between explicitness and convenience. But migrating to it from Scala 2 implicits does require major changes since possibly a large number of function signatures has to be changed to allow conversions on the arguments. This might ultimately hold back migration to Scala 3 implicits.
139177

140178
To facilitate migration, we also introduce an alternative way to specify target types of implicit conversions. We allow `into` as a soft modifier on
141179
classes and traits. If a class or trait is declared with `into`, then implicit conversions into that class or trait don't need a language import.
142180

143-
Example:
181+
For instance, the Laminar framework
182+
defines a trait `Modifier` that is commonly used as a parameter type of user-defined methods and that should support implicit conversions into it.
183+
`Modifier` is commonly used as a parameter type in both Laminar framework functions and in application-level functions that use Laminar.
184+
185+
We can support implicit conversions to `Modifier`s simply by making `Modifier` an `into` trait:
144186
```scala
145-
into class Keyword
146-
given stringToKeyword: Conversion[String, Keyword] = Keyword(_)
187+
into trait Modifier ...
188+
```
189+
This means implicit `Conversion` instances with `Modifier` results can be inserted without requiring a language import.
147190

148-
val dclKeywords = Set[Keyword]("def", "val") // ok
149-
val keywords = dclKeywords + "if" + "then" + "else" // ok
191+
Here is a simplified example:
192+
```scala
193+
trait Modifier
194+
given Conversion[Option[Node], Modifier] = ...
195+
given Conversion[Seq[Node], Modifier] = ...
196+
197+
def f(x: Source, m: Modifier) = ...
198+
f(source, Some(node)) // inserts conversion
150199
```
151-
Here, all string literals are converted to `Keyword` using the given conversion `stringToKeyword`. No feature warning or error is issued since `Keyword` is declared as `into`.
152200

153-
The `into`-as-a-modifier scheme is handy in codebases that have a small set of specific types that are intended to be the targets of implicit conversions defined in the same codebase. But it can be easily abused.
154-
One should restrict the number of `into`-declared types to the absolute minimum. In particular, never make a type `into` to just cater for the
155-
possibility that someone might want to add an implicit conversion to it.
201+
The `into`-as-a-modifier scheme is handy in codebases that have a small set of specific types that are intended as the targets of implicit conversions defined in the same codebase. Laminar's `Modifier` is a typical example. But the scheme can be easily abused by making the number of `into` types too large. One should restrict the number of `into`-declared types to the absolute minimum. In particular, never make a type `into` to just cater for the possibility that someone might want to later add an implicit conversion to it.
156202

157203

158204
## Details: Conversion target types
@@ -204,3 +250,38 @@ g(1) // error
204250
The call `f("abc")` type-checks since `f`'s parameter type `T` is `into`.
205251
But the call `g("abc")` does not type-check since `g`'s parameter type `C` is not `into`. It does not matter that `C` extends a trait `T` that is `into`.
206252

253+
254+
## Why Two Different Schemes?
255+
256+
Can we make do with just one scheme instead of two? In practice this would be difficult.
257+
258+
Let's first take a look the `Expr` example, which uses into-as-a-constructor. Could it be rewritten to use into-as-a-modifier?
259+
This would mean we have to add `into` to the whole `Expr` enum. Adding it to just `Const` is not enough, since `Add` and `Neg` take `Expr` arguments, not `Const` arguments.
260+
261+
But we might not always have permission to change the `Expr` enum. For instance, `Expr` could be defined in a lower level library without implicit conversions, but later we want to make `Expr` construction convenient by eliding `Const` wrappers in some higher-level library or application. With `into` constructors, this is easy: Define the implicit conversion and facade methods that construct `Expr` trees while taking `into[Expr]` parameters.
262+
With `into` modifiers there is no way to achieve the same.
263+
264+
A possibly more important objection is that even if we could add the `into` modifier to `Expr`, it would be bad style to do so! We want to allow for implicit conversion in the very specific case where we build an `Expr` tree using the `Add` and `Neg` constructors. Our applications could have lots of other methods that take `Expr` trees, for instance to analyze them or evaluate them.
265+
We probably do not want to allow implicit conversions for the arguments of all these other methods. The `into` modifier is too unspecific to distinguish the good use case from the problematic ones.
266+
267+
On the other hand, there are also situations where into-as-a-modifier is the practical choice. To see this, consider again the `Modifier` use case in Laminar.
268+
We could avoid the `into` modifier by wrapping all `Modifier` parameters
269+
with the `into` constructor. This would be a lot more work than adding just the single `into` modifier. Worse, functions taking `Modifier` parameters are found both in the Laminar framework code and in many applications using it. The framework and the applications would have to be upgraded in lockstep. When Laminar upgrades to Scala 3 implicits, all applications would have to be rewritten, which would make such a migration very cumbersome.
270+
271+
One can try to mitigate the effort by playing with type aliases. For instance, a hypothetical future Laminar using Scala 3 conversions could rename the
272+
trait `Modifier` to `ModifierTrait` and define an alias
273+
```scala
274+
type Modifier = into[ModifierTrait]
275+
```
276+
Then the source code of applications would not have to change (unless these applications define classes directly extending `Modifier`). But that future Laminar would not be binary compatible with the current one, since the name
277+
of the original `Modifier` trait has changed. In summary, upgrading Laminar to use Scala 3 conversions could keep either source compatibility or binary compatibility but not both at the same time.
278+
279+
280+
## Syntax Changes
281+
282+
```
283+
LocalModifier ::= ... | ‘into’
284+
```
285+
286+
`into` is a soft modifier. It is only allowed on traits and classes.
287+

0 commit comments

Comments
 (0)