-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Support by-name implicits with recursive dictionaries #6050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
This is great. Thank you! |
|
yaaaaay awesome! |
|
Yes please! |
|
One potential problem with this overload/behaviour change of the by-name language feature is that I was planning on using regular byname implicit parameters to avoid constructing the instance when it's not needed. This is necessary to get good memory allocation optimisation for implicits with conditional requirements... caching of Is there any requirement for |
|
From what I understand the effective semantics is by-need, not by-name? So, what’s preventing us from replacing the |
|
@julienrf at least when provided explicitly, it behaves as by-name, not by-need. |
|
Being implicit doesn't change the by name semantics. I think it would also make sense to have lazy arguments (both implicit and explicit), but that's somewhat orthogonal to this PR. See scala/bug#240 and https://github.com/lampepfl/dotty/issues/3005 for more discussion. |
Could you share a small motivating example that currently diverges? (The example above is distilled too far for me to see the use case) |
|
@retronym https://gitlab.com/fommil/stalactite/tree/master/src/test/scala/xmlformat Try removing the Lazy, Cached and Strict here in either the generic encoder or decoder and see the recursive tests fail. I must admit, I'm confused about exactly what this means for my usecase... I'm unable to see the above in my example. Does this only replace Lazy? What about Cached and Strict? What's the implication to performance, because if these things really do become by-name it's going to kill performance when the lazy semantics are not necessary. I have a good usecase for byname implicits, which does help performance, but added across the board this will be terrible. |
|
@retronym there are motivating examples in the tests. Without the ability to tie the knot here the compiler would diverge on seeing the occurrence of |
|
@fommil |
|
2.13 is a better target for this work. 2.12 is now at a point where we need to focus on stability and bug compatibility. |
|
I have an alternative mechanism (to shapeless Generic / LabelledGeneric) for typeclass derivation that also hits the recursive types problem. I'll be interested to see if this fixes it. In the meantime I can use Lazy and Cached, hopefully without typeclass authors or end users needing to know about it. |
|
I've been playing around with this branch and found some variation between Dotty and this PR. Here is an isolated example (adapted from shapeless-type-class-derivation-2015-demo) that compiles using Dotty, but diverges on this PR. I suspect the difference comes from the divergence checking. |
|
@OlivierBlanvillain That's a very useful example. Thanks! It wouldn't expect the You're quite right that it's a difference in the divergence checker which is responsible for the difference here. I've implemented a slightly more sophisticated test than the user specified upper bound that you've gone for in Dotty. I think that it would be better to avoid making the limit a compilation parameter if at all possible. I'm putting together a SIP for this at the moment and I'll include the details of the divergence criteria along with a proof that implicit search either terminates successfully or fails reporting divergence. I'm fairly confident that you will be able to port it over to the Dotty implementation. |
87b912c to
8fe649d
Compare
|
Rebased to 2.13.x. @OlivierBlanvillain your example now works as is, but also with just the generic instance having a byname argument ... see test/files/pos/byname-implicits-13.scala and test/files/pos/byname-implicits-14.scala. |
|
@milessabin I just compiled all the pos & run tests from this PR on dotty master and it seams that both compilers agree as far as bymane implicits are concerned 👍 |
|
@OlivierBlanvillain oh, awesome :-) |
This comment has been minimized.
This comment has been minimized.
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.
|
@retronym those changes look fine. I'll rebase and pull them in to this PR. |
This is the new way to freshen names while keeping compiler output stable when sources are re-ordered or incrementally recompiled.
An implicit macro within the by-name implicit dictionary might return a typechecked tree, so we ought to use changeOwner when moving that code into the RHS of a synthetic member of `LazyDefns`
|
@retronym there are a couple of what look to be unrelated commits in there: retronym@e3cf13e and retronym@67a8665. I'll leave them out. |
fdc4261 to
882881b
Compare
Incorporated changes suggested by @retronym.
|
Rebased and incorporated @retronym's suggestions. |
|
@adriaanm I think this is ready to merge now :-) |
|
🍰 |
|
😋 |
|
Amazing work, thank you so much @milessabin and everyone involved! 2.13 is going to be so great! |
Assigned number 36 to SIP “By-name implicit arguments”. Implemented in Scala 2.13 in scala/scala#6050 Implemented in Scala 3 in scala/scala3#5461
Assigned number 36 to SIP “By-name implicit arguments”. Implemented in Scala 2.13 in scala/scala#6050 Implemented in Scala 3 in scala/scala3#5461
This Typelevel Scala collaboration sponsored by Lightbend ...
This is an implementation of byname implicit parameters with recursive dictionaries, intended as a language-level replacement for shapeless's
Lazytype. As of this PR, 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.There is a SIP describing this language change in greater detail here.
Consider the following example,
In current Scala this would diverge due to the recursive implicit argument
recof methodfoo. Under the scheme implemented in this PR we can mark the recursive implicit parameter as byname,and recursive occurrences of this sort are extracted out as
valmembers of a local synthetic object as follows,The example now compiles with the assertion successful. Note that the recursive use of
rec$1occurs within the byname argument offooand 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
Lazyin shapeless,Lazyin shapeless is extremely delicate, relying on non-portable compiler internals. By contrast there is already an initial implementation of byname implicits in Dotty: Let by-name implicit parameters have lazy semantics scala3#1998.Lazycan cause the typechecker to loop indefinitely. The byname implicits implementation is able to both solve recursive occurrences and check for divergence.Lazyinterferes with the heuristics for solving inductive implicits in Topic/inductive implicits 2.13.x #6481 because the latter depends on being able to verify that induction steps strictly reduce the size of the types being solved for; the additionalLazytype 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.Lazythan would have to be marked as byname under this PR due to restrictions on what theLazymacro is able to do. Given that there is a runtime cost associated with capturing the thunks required for bothLazyand byname arguments, any reduction in the number is beneficial.