This thread is the SIP Committee’s request for comments on a proposal to revise implicit parameters in Scala. It forms part of a set of proposals that are collected under “Reference/Contextual Abstractions” in the Dotty Docs. Each of these proposals will get a separate discussion thread here. The relevant sections for the current proposal are Inferable Parameters and parts of Relationship with Scala-2 Implicits. Here’s a summary of the proposal that excerpts from these sections.
Note: To channel discussions, we are going to deal with individual “Contextual Abstractions” proposals in separate threads. But it would be good to read and absorb all proposals there together before starting to discuss.
Inferable Parameters
Functional programming tends to express most dependencies as simple function parameterization.
This is clean and powerful, but it sometimes leads to functions that take many parameters and
call trees where the same value is passed over and over again in long call chains to many
functions. Inferable parameters can help here since they enable the compiler to synthesize
repetitive arguments instead of the programmer having to write them explicitly.
For example, a maximum function that works for any arguments for which an ordering exists can be defined as follows:
def max[T](x: T, y: T) given (ord: Ord[T]): T =
if (ord.compare(x, y) < 1) y else x
Here, ord
is an inferable parameter. Inferable parameters are introduced with a given
clause.
The max
method can be applied as follows:
max(2, 3) given IntOrd
The given IntOrd
part provides the IntOrd
instance as an argument for the ord
parameter. But the point of inferable parameters is that this argument can also be left out (and it usually is). So the following applications are equally valid:
max(2, 3)
max(List(1, 2, 3), Nil)
Anonymous Inferable Parameters
In many situations, the name of an inferable parameter of a method need not be
mentioned explicitly at all, since it is only used in synthesized arguments for
other inferable parameters. In that case one can avoid defining a parameter name
and just provide its type. Example:
def maximum[T](xs: List[T]) given Ord[T]: T =
xs.reduceLeft(max)
maximum
takes an inferable parameter of type Ord
only to pass it on as an
inferred argument to max
. The name of the parameter is left out.
Generally, inferable parameters may be given either as a parameter list
(p_1: T_1, ..., p_n: T_n)
or as a sequence of types, separated by commas.
Inferring Complex Arguments
Here are two other methods that have an inferable parameter of type Ord[T]
:
def descending[T] given (asc: Ord[T]): Ord[T] = new Ord[T] {
def compare(x: T, y: T) = asc.compare(y, x)
}
def minimum[T](xs: List[T]) given Ord[T] =
maximum(xs) given descending
The minimum
method’s right hand side passes descending
as an explicit argument to maximum(xs)
. With this setup, the following calls are all well-formed, and they all normalize to the last one:
minimum(xs)
maximum(xs) given descending
maximum(xs) given (descending given ListOrd)
maximum(xs) given (descending given (ListOrd given IntOrd))
Mixing Inferable And Normal Parameters
Inferable parameters can be freely mixed with normal parameters.
An inferable parameter may be followed by a normal parameter and vice versa.
There can be several inferable parameter lists in a definition. Example:
def f given (u: Universe) (x: u.T) given Context = ...
implied global for Universe { type T = String ... }
implied ctx for Context { ... }
Then the following calls are all valid (and normalize to the last one)
f("abc")
(f given global)("abc")
f("abc") given ctx
(f given global)("abc") given ctx
Syntax
Here is the new syntax of parameters and arguments seen as a delta from the standard context free syntax of Scala 3.
ClsParamClause ::= ...
| ‘given’ (‘(’ ClsParams ‘)’ | GivenTypes)
DefParamClause ::= ...
| GivenParamClause
GivenParamClause ::= ‘given’ (‘(’ DefParams ‘)’ | GivenTypes)
GivenTypes ::= AnnotType {‘,’ AnnotType}
InfixExpr ::= ...
| InfixExpr ‘given’ (InfixExpr | ParArgumentExprs)
Relationship with current Implicits
The new inferable parameter syntax with given
corresponds largely to Scala-2’s implicit parameters. E.g.
def max[T](x: T, y: T) given (ord: Ord[T]): T
would be written
def max[T](x: T, y: T)(implicit ord: Ord[T]): T
in Scala 2. The main difference concerns applications of such parameters. Explicit arguments to inferable parameters must be written using given
, mirroring the definition syntax. E.g, max(2, 3) given IntOrd
. Scala 2 uses normal applications max(2, 3)(IntOrd)
instead. The Scala 2 syntax has some inherent ambiguities and restrictions which are overcome by the new syntax. For instance, multiple implicit parameter lists are not available in the old syntax. They require instead a complex emulation using auxiliary objects in the “Aux” pattern.
Context Bounds
Context bounds are the same in both language versions. They expand to the respective forms of implicit parameters.
Note: To ease migration, context bounds in Dotty map for a limited time to old-style implicit parameters for which arguments can be passed either with given
or with a normal application. Once old-style implicits are deprecated, context bounds will map to given parameters instead.
Implementation Status and Timeline
The Dotty implementation implements both old-style implicit parameters and new-style given
parameters. In fact, support for Scala-2’s implicits is an essential part of the common language subset between 2.13/2.14 and Dotty. Migration to the new abstractions will be supported by making automatic rewritings available.
It is planned to deprecate and phase out old-style implicit parameters in a version following Scala 3.0. The precise timeline will depend on adoption patterns.