This Pre-SIP proposes to go ahead with restricting implicit conversions. The topic has already been extensively discussed in Can We Wean Scala Off Implicit Conversions?. That thread was already very long, so am opening a new thread here.
Why Drop Unrestricted Implicit Conversions?
They are simply too dangerous. They might kick in at unforeseen points, and have unforeseen effects.
Compare with normal implicit parameters: Here we know a method expects an argument to be inferred and we know its type. So we know what to watch out for and we have a good idea what the shape of the inferred term will be.
But implicit conversions could kick in anywhere where two types don’t match. They might hide type errors. Even if they are used as intended they make it hard for a reader of the code to figure out what goes on.
Another important reason is that unrestricted implicit conversions make type inference less precise and less efficient. The problem is that we can never be sure whether some type in the context constrains a type variable. An implicit conversion might be inserted that makes the two types unrelated. This means we cannot propagate info as thoroughly as we would like. Also, where we do propagate we need to be prepared to backtrack and try different propagation strategies. This means duplicated work and slower type inference. The price of lower quality type inference is incurred even if no implicit conversions are inserted at all since the compiler cannot currently know that beforehand.
The previous thread discusses this in more detail.
Where Are Implicit Conversions Hard to Replace?
From the discussion on the previous thread emerged three areas where implicit conversions are currently difficult to replace. These are:
-
When they are used as argument conversions to support variation in possible argument types. Such situations are pervasive in the standard library. For example:
class Iterable[+A]: def concat(xs: IterableOnce[A]): Iterable[A] = ...
Here, we also want to be able to pass an array to
concat
, or pass a string if theIterable
's element type isChar
. These generalizations are achieved by defining implicit conversions from arrays and strings to sequences. A similar idea is the essence of the magnet pattern in libraries such as Akka Http. -
When they support bulk extensions. The implicit conversion from array to sequence has the welcome side-effect that it makes all sequence operations available on arrays. Without it, we’d have to define forwarders for all sequence operations manually as extension methods. Since there are over a hundred such methods, this is very tedious and hard to maintain. By contrast, the implicit conversion target can inherit most of these operations from base traits, so the effort to set this up is much lower.
-
When doing design exploration. In an exploration phase, we might welcome the looser typing provided by implicit conversions. They help keep the code short and adapt easily to changes in types. For instance, a result of some method might be a
String
or aText
object. If there is an implicit conversion fromString
toText
, we can change the result type without having to change the body of the method.
How Can We Address This?
Old-style implicit conversions will be deprecated over the next versions of the language. New-style Conversion
instances already require a language import and that import needs to be given at the point where they are inserted. (Old style conversions needed a language import at the point where they were defined, which turned out to be useless). If no language import is given, the compiler emits a feature warning.
I propose to tighten the rules in some future Scala 3.x version so that the language import for new-style Conversion
instances becomes mandatory. If none is given at the point where they are inserted the compiler will then emit not just a feature warning but an an error. This means that code without the language import cannot have implicit-conversions inserted in arbitrary places. Therefore, we can use improved type-inference algorithms for such code that lead to better inferred types and better compile times.
To be able to do this, we need to address the three areas where implicit conversions were hard to replace. I propose to do this with a mixture of some smallish language extensions, library extensions, and tooling.
1. Argument Conversions
Argument conversions could in principle be replaced with typeclasses, but the change would be very disruptive to library designs and the resulting method signatures would become more complicated. Instead of going down that path, I propose to annotate parameters where argument conversions are allowed. Example:
class Iterable[+A]:
def concat(xs: ~IterableOnce[A]): Iterable[A] = ...
Here, the ~
in front of the IterableOnce
type indicates that we accept not only instances of IterableOnce
but also values that can be implicitly converted to it. Implicit conversions can then be inserted for arguments of concat
without a language import. This annotation makes argument conversions as predictable as other implicit arguments: the method definition tells us where they are allowed. The ~
syntax is not definite, it should be seen more as an example how we might want to express this.
2. Bulk Extensions
Extension methods are intended to replace in Scala 3 implicit classes and other implicit conversions. But implicit classes and conversions can support bulk extension through inheritance whereas current extension methods cannot. I propose to change this with two language tweaks.
- Allow export clauses in collective extensions.
- Allow the qualifier of an export clause to be an expression instead of a stable identifier.
This would let us write code like:
extension [A](xs: Array[A])
export arrayOps(xs).*
This extension defines every method of arrayOps
as an extension method for arrays. So where implicit classes use inheritance plus implicit conversions to achieve bulk extension, this new mechanism would use just aggregation, in the form it is provided by exports.
3. Explorative Programming
Explorative programming can still be supported by importing language.implicitConversions
. To switch from exploration to stable code, one could have a tool or compiler setting that makes all implicitly inserted conversions explicit so that the language import can be dropped. To make explicit conversions nicer to use, we should offer an extension method inject
in the Conversion
class, like this:
abstract class Conversion[-T, +U] extends Function1[T, U]:
def apply(x: T): U
object Conversion:
extension [T](x: T) def inject[U]: U = apply(x)
Then an explicit conversion of a value a
to a type B
can be summoned by a.inject[B]
. Or, if B
can be inferred, it’s just a.inject
.
Timeline
As a first step, we should add the necessary language and library extensions so that we can experiment with them. This could happen already in 3.1.
If things work out well, we could start making use-site language imports mandatory in 3.2.
The type inferencer could be upgraded one version later. The reason we cannot upgrade the type inferencer at the same time as the mandatory language import is that it would obscure error messages where implicit conversions were expected to be inserted.