You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
11
11
importscala.language.experimental.into
12
12
```
13
13
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
+
14
29
Scala 3's implicit conversions of the `scala.Conversion` class require a language import
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.
31
46
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:
33
50
34
51
```scala
35
52
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
44
61
45
62
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.
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`.
56
73
57
-
## `into` in Function Results
74
+
### Example 2
75
+
76
+
Consider a simple expression AST type:
77
+
```scala
78
+
enumExpr:
79
+
caseNeg(e: Expr)
80
+
caseAdd(e1: Expr, e2: Expr)
81
+
caseConst(n: Int)
82
+
importExpr.*
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
+
givenConversion[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
+
enumExpr:
91
+
caseNeg(e: into[Expr])
92
+
caseAdd(e1: into[Expr], e2: into[Expr])
93
+
caseConst(n: Int)
94
+
```
95
+
This would allow conversions from `Int` to `Const` when constructing trees but not elsewhere.
96
+
97
+
### `into` in Function Results
58
98
59
99
`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]`:
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`.
72
112
73
-
## Vararg arguments
113
+
###Vararg arguments
74
114
75
115
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
76
116
number of `IterableOnce[Char]` arguments, and also allows implicit conversions into `IterableOnce[Char]`:
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.
87
127
88
128
89
-
## Unwrapping `into`
129
+
###Unwrapping `into`
90
130
91
131
Since `into[T]` is an opaque type, its run-time representation is just `T`.
92
132
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
107
147
108
148
109
149
110
-
## Dropping `into` for Parameters in Method Bodies
150
+
###Dropping `into` for Parameters in Method Bodies
111
151
112
152
The typical use cases for `into` wrappers are for parameters. Here, they specify that the
113
153
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
123
163
124
164
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.
125
165
166
+
### Into Constructors in Type Aliases
126
167
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
131
169
```scala
132
-
typeModifier= into[ModifierClass]
170
+
typeToIterator[T]= into[IterableOnce[T]]
133
171
```
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.
135
173
136
-
## Alternative: `into` as a Modifier
174
+
## Second Scheme: `into` as a Modifier
137
175
138
176
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.
139
177
140
178
To facilitate migration, we also introduce an alternative way to specify target types of implicit conversions. We allow `into` as a soft modifier on
141
179
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.
142
180
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:
This means implicit `Conversion` instances with `Modifier` results can be inserted without requiring a language import.
147
190
148
-
valdclKeywords=Set[Keyword]("def", "val") // ok
149
-
valkeywords= dclKeywords +"if"+"then"+"else"// ok
191
+
Here is a simplified example:
192
+
```scala
193
+
traitModifier
194
+
givenConversion[Option[Node], Modifier] = ...
195
+
givenConversion[Seq[Node], Modifier] = ...
196
+
197
+
deff(x: Source, m: Modifier) = ...
198
+
f(source, Some(node)) // inserts conversion
150
199
```
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`.
152
200
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.
156
202
157
203
158
204
## Details: Conversion target types
@@ -204,3 +250,38 @@ g(1) // error
204
250
The call `f("abc")` type-checks since `f`'s parameter type `T` is `into`.
205
251
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`.
206
252
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
+
typeModifier= 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.
0 commit comments