The `into` Type Modifier
Scala 3's implicit conversions of the scala.Conversion
class require a language import
import scala.language.implicitConversions
in any code that uses them as implicit conversions (code that calls conversions explicitly is not affected). If the import is missing, a feature warning is currently issued, and this will become an error in a future version of Scala 3. The motivation for this restriction is that code with hidden implicit conversions is hard to understand and might have correctness or performance problems that go undetected.
There is one broad use case, however, where implicit conversions are very hard to replace. This is the case where an implicit conversion is used to adapt a method argument to its formal parameter type. An example from the standard library:
scala> val xs = List(0, 1)
scala> val ys = Array(2, 3)
scala> xs ++ ys
val res0: List[Int] = List(0, 1, 2, 3)
The last input made use of an implicit conversion from Array[Int]
to IterableOnce[Int]
which is defined as a Scala 2 style implicit conversion in the standard library. 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.
This is where the into
modifier on parameter types comes in. Here is a signature of the ++
method on List[A]
that uses it:
def ++ (elems: into IterableOnce[A]): List[A]
The into
modifier on the type of elems
means that implicit conversions can be applied to convert the actual argument to an IterableOnce
value, and this without needing a language import.
Function arguments
into
also allows conversions on the results of function arguments. For instance, consider the new proposed signature of the flatMap
method on List[A]
:
def flatMap[B](f: A => into IterableOnce[B]): List[B]
This accepts all actual arguments f
that, when applied to an A
, give a result that is convertible to IterableOnce[B]
. So the following would work:
scala> val xs = List(1, 2, 3)
scala> xs.flatMap(x => x.toString * x)
val res2: List[Char] = List(1, 2, 2, 3, 3, 3)
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
.
Vararg arguments
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 number of IterableOnce[Char]
arguments, and also allows implicit conversions into IterableOnce[Char]
:
def concatAll(xss: (into IterableOnce[Char])*): List[Char] =
xss.foldLeft(List[Char]())(_ ++ _)
Here, the call
concatAll(List('a'), "bc", Array('d', 'e'))
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.
Note that a vararg parameter type with into modifiers needs to be put in parentheses, as is shown in the example above. This is to make the precedence clear: each element of the argument sequence is converted by itself.
Retrofitting Scala 2 libraries
There is also an annotation @into
in the scala.annotation
package that has the same effect as an into
modifier. It is intended to be used for retrofitting Scala 2 library code so that Scala 3 conversions can be applied to arguments without language imports. For instance, the definitions of ++
and flatMap
in the Scala 2.13 List
class could be retrofitted as follows.
def ++ (elems: IterableOnce[A] @into): List[A]
def flatMap[B](f: A => IterableOnce[B] @into): List[B]
For Scala 3 code, the into
modifier is preferred, because it adheres to the principle that annotations should not influence typing and type inference in Scala.
Restrictions
The into
modifier is only allowed in the types of method parameters. It can be given either for the whole type, or some result type of a top-level function type, but not anywhere else. The into
modifier does not propagate outside the method. In particular, a partially applied method does not propagate into
modifiers to its result.
Example:
Say we have
def f(x: Int)(y: into Text): Unit
then
f(3) : Text => Unit
Note the into
modifier is not longer present on the type of f(3)
. Therefore, follow-on arguments to f(3)
do not allow implicit conversions. Generally it is not possible to define function types that allow implicit conversions on their arguments, but it is possible to define SAM types that allow conversions. E.g.
trait ConvArg:
def apply(x: into Text): Unit
val x: ConvArg = f(3)(_)
Note this is similar to the way vararg parameters are handled in Scala. If we have
def g(x: Int)(y: Int*): Unit
then
g(4) : Seq[Int] => Unit
Observe that the vararg annotation also got dropped in the result type of g(4)
.
Syntax changes
The addition to the grammar is:
ParamType ::= [‘=>’] ParamValueType
ParamValueType ::= Type [‘*’]
| IntoType
| ‘(’ IntoType ‘)’ ‘*’
IntoType ::= [‘into’] IntoTargetType
| ‘(’ IntoType ‘)’
IntoTargetType ::= Type
| FunTypeArgs (‘=>’ | ‘?=>’) IntoType
As the grammar shows, into
can only applied in the type of a parameter; it is illegal in other positions. Also, into
modifiers in vararg types have to be enclosed in parentheses.