This is an implementation of byname implicit parameters with recursive
dictionaries, intended as a language-level replacement for shapeless's
Lazy type. As of this commit, implicit arguments can be marked as byname
and at call sites recursive uses of implicit values are permitted if
they occur in an implicit byname argument.
Consider the following example,
trait Foo { def next: Foo }
object Foo {
implicit def foo(implicit rec: Foo): Foo =
new Foo { def next = rec }
}
val foo = implicitly[Foo]
assert(foo eq foo.next)
In current Scala this would diverge due to the recursive implicit
argument rec of method foo. Under the scheme implemented in this commit
we can mark the recursive implicit parameter as byname,
trait Foo { def next: Foo }
object Foo {
implicit def foo(implicit rec: => Foo): Foo =
new Foo { def next = rec }
}
val foo = implicitly[Foo]
assert(foo eq foo.next)
and recursive occurrences of this sort are extracted out as val members
of a local synthetic object as follows,
val foo: Foo = scala.Predef.implicitly[Foo](
{
object LazyDefns$1 {
val rec$1: Foo = Foo.foo(rec$1)
// ^^^^^
// recursive knot tied here
}
LazyDefns$1.rec$1
}
)
assert(foo eq foo.next)
and the example compiles with the assertion successful. Note that the
recursive use of rec$1 occurs within the byname argument of foo and is
consequently deferred. The desugaring matches what a programmer would do
to construct such a recursive value explicitly.
This general pattern is essential to the derivation of type class
instances for recursive data types, one of shapeless's most common
applications.
Byname implicits have a number of benefits over the macro implementation
of Lazy in shapeless,
+ the implementation of Lazy in shapeless is extremely delicate, relying
on non-portable compiler internals. By contrast there is already an
initial implementation of byname implicits in Dotty:
scala/scala3#1998.
+ the shapeless implementation is unable to modify divergence checking,
so to solve recursive instances it effectively disables divergence
checking altogether ... this means that incautious use of Lazy can cause
the typechecker to loop indefinitely. The byname implicits
implementation is able to both solve recursive occurrences and check for
divergence.
+ the implementation of Lazy interferes with the heuristics for solving
inductive implicits in scala#6481 because the latter depends on being able to
verify that induction steps strictly reduce the size of the types being
solved for; the additional Lazy type constructors make the type appear
be non-decreasing in size. Whilst this could be special-cased, doing so
would require some knowledge of shapeless to be incorporated into the
compiler. Being a language-level feature, byname implicits can be
accommodated directly in the induction heuristics.
+ in common cases more implicit arguments would have to be marked as
Lazy than would have to be marked as byname under this PR due to
restrictions on what the Lazy macro is able to do. Given that there is a
runtime cost associated with capturing the thunks required for both Lazy
and byname arguments, any reduction in the number is beneficial.