-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #6687: handle gadt bounds in match type reduction #7389
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
@OlivierBlanvillain as things stand that I'd be happy to replace if there's a suitable alternative now. Can you suggest a approach which will work after this PR? |
c4fb119
to
f355c0c
Compare
@milessabin I'll to think about it but it might help to have a bit more context, where/why do you need that trick? I have a feeling that there might be something fundamentally impossible about what this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look good to me. I agree that it must be superType
instead of underlying
. We should dig deeper what we need to do to recover shapeless compilation.
The version for kind type Id[t] = t
type Const[c] = [t] =>> c
case class Wrap[T](t: T)
class Dummy
type Apply[T[_]] = T[Dummy]
type Unapply[F[_[_]], T] = T match {
case Wrap[Apply[a]] => F[a]
case Wrap[Dummy] => F[Id]
case Wrap[c] => F[Const[c]]
}
type LiftP[F[_[_]], T[_]] = LiftP0[F, Apply[T]]
type LiftP0[F[_[_]], T] <: Tuple = T match {
case Unit => Unit
case (a *: b) => Unapply[F, Wrap[a]] *: LiftP0[F, b]
} The idea is that given, trait Functor[_[_]]
type Repr[t] = (Option[t], t, Int) we should have, LiftP[Functor, Repr] =:= (Functor[Option], Functor[Id], Functor[Const[Int]]) This works very nicely prior to this PR, and could be encoded at the term level using givens. I'd be happy to use any mechanism which allowed me to express this at the type level without the need for intermediary terms. |
As an addendum to my previous comment, I have no particular attachment to the type Unapply[F[_[_]], T[_]] = T[Dummy] match {
case Dummy => F[Id]
case a[Dummy] => F[a]
case c => F[Const[c]]
} Clearly for this to work matches would have to be sequential and potentially overlapping, and we would also have to refuse to instantiate I think there's a good case for sequential and overlapping match semantics, if only to keep alignment between term and type matches. I see that preventing type Unapply[F[_[_]], T[_]] = T[Dummy] match {
case Dummy => F[Id]
case Apply[a, Dummy] => F[a]
case c => F[Const[c]]
} |
What about teaching to the compiler to do exactly that? In T[Dummy] match {
case Dummy => F[Id]
case a[Dummy] => F[a]
case c => F[Const[c]]
} If |
That certainly works in this case. |
Indeed, I can imagine you could also arrange for the compiler to instantiate But, and it's a big but, this is just one example for one kind. In shapeless we need this to be either open to the addition of support for additional kinds by users (what we have now), or generalized for all kinds. To give a flavour of this, the following are the equivalents for kinds // (* -> *) -> *
type Id[t] = [f[_]] =>> f[t]
type Const[c] = [f[_]] =>> c
class Dummy[T]
type Apply[T[_[_]]] = T[Dummy]
type Unapply[F[_[_[_]]], T] = T match {
case Wrap[Apply[a]] => F[a]
case Wrap[Dummy[a]] => F[Id[a]]
case Wrap[c] => F[Const[c]]
}
type LiftP[F[_[_[_]]], T[_[_]]] = LiftP0[F, Apply[T]]
type LiftP0[F[_[_[_]]], T] <: Tuple = T match {
case Unit => Unit
case (a *: b) => Unapply[F, Wrap[a]] *: LiftP0[F, b]
}
// * -> * -> *
type Id1[t, u] = t
type Id2[t, u] = u
type Const[c] = [t, u] =>> c
class Dummy1
class Dummy2
type Apply[T[_, _]] = T[Dummy1, Dummy2]
type Unapply[F[_[_, _]], T] = T match {
case Wrap[Apply[a]] => F[a]
case Wrap[Dummy1] => F[Id1]
case Wrap[Dummy2] => F[Id2]
case Wrap[c] => F[Const[c]]
}
type LiftP[F[_[_, _]], T[_, _]] = LiftP0[F, Apply[T]]
type LiftP0[F[_[_, _]], T] <: Tuple = T match {
case Unit => Unit
case (a *: b) => Unapply[F, Wrap[a]] *: LiftP0[F, b]
} Note that the kinds of I think this can be automated in a principled way, but it's not quite as straightforward as the |
Yes, but not that case,
I see. So, to support these other kinds, all we need would be the ability to synthesize I'm going to spend a bit more time trying to convince TypeComparer of doing that... But it turns out too complicated, what do you think about these alternatives:
|
That sounds about right.
Another fallback idea: all we're really trying to do here is split a type into a non-trivial application of a type constructor to a type. If it's too complicated to bake this into |
Having Apply/Unapply as a primitive sounds like the best option. The fact that |
f355c0c
to
d7c581c
Compare
Thanks for the rebase ... I think something like the following is the primitive we need: a pair of kind-polymorphic match types object Test {
// Simulation of primitive kind-polymorphic Head/Tail for kind * -> *
class Apply1[F[_]]
// hard-coded types for illustration only
type P0 = [t] =>> (t, List[t], Int)
type P1 = [t] =>> (List[t], Int)
type P2 = [t] =>> Tuple1[Int]
type Head[F[_]] <: ([t] =>> Any) = Apply1[F] match {
case Apply1[P0] => [t] =>> t
case Apply1[P1] => List
case Apply1[P2] => [t] =>> Int
}
type Tail[F[_]] <: ([t] =>> Any) = Apply1[F] match {
case Apply1[P0] => P1
case Apply1[P1] => P2
case Apply1[P2] => [t] =>> Unit
}
// shapeless library code
type Id[t] = t
type Const[C] = [t] =>> C
type Nil = [t] =>> Unit
type LiftP[F[_[_]], T[_]] <: Tuple = Apply1[T] match {
case Apply1[Nil] => Unit
case _ => F[Head[T]] *: LiftP[F, Tail[T]]
}
// shapeless client code
trait Functor[F[_]]
type Repr[t] = (t, List[t], Int)
summon[LiftP[Functor, Repr] =:= (Functor[Id], Functor[List], Functor[Const[Int]])]
} |
Instead of creating special type LiftPK[_[_]] = ... // compiler-provided
type HeadPK[t] = LiftPK[Head][t] // consistent behaviour with Miles' Head
Head[(1,2,3)] === 1
HeadPK[(1,2,3)] === 1
HeadPK[[t] => (t, 2, 3)]] === [t] => t
type TailPK[t] = LiftPK[Tail][t] // similarly, has behaviour consistent with Tail from the preceding post As for compiler implementation (and probably as a reminer for @OlivierBlanvillain), |
5ed4828
to
1128d81
Compare
The Unapply match type only worked because of an unsound behavior in match type reduction. To move forward with HK type class derivation we need to find an alternative to the Apply/Unapply trick, either using implicits/macros or by adding a new primitive match type to the compiler. See discussion in scala/scala3#7389.
1128d81
to
a92dc61
Compare
This appears to be commenting out roughly 3/4 of shapeless (ie. pretty much everything higher-kinded) to pass the community build. If we go down the |
The situation is the same as it was one year ago when we first discussed it on this PR. To summarize, this PR uncovered a soundness bug in match type reduction. Unfortunately, shapeless3 heavily relies on the unsoundness in its I don't think the work on a new primitive type-level lifting operator belongs to this PR. I also think this work should be done in conjunction with the shapeless3 changes, or we risk ending up with a new very advanced type operator that doesn't serve its primary purpose... From a dotty perspective, there is still value in what's left of shapeless tests after the breakage: type class derivation for |
My 2 cents from open source ecosystem perspective: given the substantial impact to Shapeless and the timeline for 3.0.0 RC1, this PR should be deferred until a suitable alternative is developed and integrated with Shapeless 3. If that can fit in before 3.0.0 RC1, great, but otherwise should be deferred. |
Is the ecosystem already depending on shapeless3? I think it was a mistake not to merge this a year ago and delaying it any further just makes the situation worse. One compromise to move forward is to keep the unsound behavior under a |
I agree that the soundness hole should be plugged, and the way that Shapeless is exploiting it is definitely unsound even at a glance. However, it's worth putting in the effort to provide sound functionality which serves the same purpose. While Shapeless 3 is not yet heavily depended upon by the Scala 3 ecosystem, it is representative of most of the things that are required to apply Scala 3's metaprogramming capabilities to useful problems. Libraries like Circe and Scodec have, in practice, re-tread the same paths in numerous ways. As the ecosystem expands and more higher-kinded applications of the system are attempted, the limitation encoded here, without the amelioration of something like I would invite everyone to view this less in terms of "crippling one library" and more in terms of "removing fundamental functionality from the language which is necessary to apply a core feature (metaprogramming) in a general fashion". The fact that Shapeless is the only current member of the ecosystem which uses that fundamental functionality is representative of adoption stage of the language, not representative of significance. |
26518ca
to
0bd6ebe
Compare
@OlivierBlanvillain @milessabin Do I understand correctly that explicitly representing shapes and interpreting them would still work with this PR? sealed trait Shape
class ID extends Shape
class CONST[t] extends Shape
class APP[f[_]] extends Shape
type Id[t] = t
type Const[c] = [t] =>> c
type Interp[F[_[_]], T <: Shape] = T match {
case ID => F[Id]
case CONST[t] => F[Const[t]]
case APP[g] => F[g]
}
type LiftP[F[_[_]], T] <: Tuple = T match {
case EmptyTuple => EmptyTuple
case (a *: b) => Interp[F, a] *: LiftP[F, b]
}
// user code
trait Functor[F[_]]
// Now Repr is of kind *
type Repr = (APP[Option], ID, CONST[Int])
implicitly[LiftP[Functor, Repr] =:= (Functor[Option], Functor[Id], Functor[Const[Int]])] |
@b-studios I don't know if this fits all the use cases in shapeless, but it seems by far to be the cleanest option! Maybe a possible alternative could be to rely on implicit search and |
@b-studios the problem is with your suggested, // Now Repr is of kind *
type Repr = (APP[Option], ID, CONST[Int]) and this explains why this change is crippling for Dotty's type class derivation infrastructure.
type MirroredElemTypes[t] = (Option[t], t, Int) We could only rewrite this in the form you're suggesting if we added those wrapper types to the standard library and, (literally) infinitely worse, all the additional wrapper types needed for all possible product type constructor signatures. I was very careful in the design of Dotty's mirrors to ensure that such an explosion would be unnecessary. The ability to define something like To be clear, this doesn't merely do significant damage to shapeless 3, it also severely compromises Scala 3's type class derivation infrastructure. |
I, uh, am not entirely convinced that being able to switch to an unsound match type reduction algorithm with a -Y flag is the best of approaches. In particular, all Shapeless3 users would need to opt into that flag to make use of HK features (if I understand correctly), and seeing as that'd make them use an unsound match type reduction algorithm, that's not very good. I think it's important that we ensure Shapeless3 is working and functional when we finally release Scala 3, and there's still the possibility of adding a primitive like |
Not a huge amount. Shapeless has rough equivalents to If I have the primitive I can find the time to do that. |
That all sounds good to me, but I don't think the work on To reiterate, the compiler is currently broken and the more we wait, the worse things get since more and more users might depend on the current unsound behavior. This is also getting rather annoying to maintain; I've already rebased this PR multiple times, and every time I minimize a match type issue, I have to checkout this branch to see if the bug still exists here. @abgruszecki To be clear, the added Y flag is not a long term solution and is not intended to be used by anyone but shapeless3 in the community build. It is merely a compromise to move forward with the PR without losing the benefits of compiling the shapeless3 prototype on every PR. There is also the option of releasing a broken compiler and delaying the fix every further, potentially hindering every match type user, is this what people are suggesting? |
I think the problem mentioned by @abgruszecki is that if shapeless needs this flag to compile, then users of shapeless will also need that flag to use its match types, so it will propagate through the ecosystem. |
Yes, that's basically it. If the flag is not intended to be a long-term solution, then how long do we intend to keep it? It seems dangerous to me to even have it in M3, though rather less dangerous than having unsound match types. I just think that it's important that we commit somehow to ensuring that both Shapeless3 and typeclass derivation are at their best for the final Scala3 release. |
The Unapply match type only worked because of an unsound behavior in match type reduction. To move forward with HK type class derivation we need to find an alternative to the Apply/Unapply trick, either using implicits/macros or by adding a new primitive match type to the compiler. See discussion in scala/scala3#7389.
@smarter @abgruszecki If you are not convinced about the Y flag then I'll go back to my original proposal with commented out tests. Since I started reviving this PR I already got 3 different conflicts on the shapeless submodule, so there is clearly a lot of value of keeping it there as it is. |
0bd6ebe
to
3d31536
Compare
By removing the widenAbstractTypes step, which is subsumed by the provably empty test.
Underlying breaks kinds (the underlying of an AppliedType is a HKTypeLambda). The edited test case was making use of that bug to implement an impossible match type. Indeed, that test is trying to distinguish f[Dummy] (for some f) and Int But there is no way to tell that these two types are disjoint. In fact they are not, since `type F[X] = Any` matches the `f[Dummy]` pattern and overlaps with `Int`.
3d31536
to
54fe387
Compare
After further discussion we decided to merge this PR with the Y flag, knowing that this is only a temporary solution and the flag will be removed by the 3.0 release. The work on the |
The Unapply match type only worked because of an unsound behavior in match type reduction. To move forward with HK type class derivation we need to find an alternative to the Apply/Unapply trick, either using implicits/macros or by adding a new primitive match type to the compiler. See discussion in scala/scala3#7389.
on top of #7364
Handling GADT bounds in the match type reduction algorithm turns out to be relatively straightforward, it boils down to adding two extra cases in
provablyDisjoint
as done in the last commit cd55b20.However doing so requires to get rid of the
widenAbstractTypes
check, as this check can preemptively rule out pair of types that can be proved disjoint using GADT information. I believe after #7364 this check is redundant and can therefore be removed entierly, as done in the "Simplify MT reduction algorithm" commit.Remove that check uncovered another bug in the
provablyDisjoint
algorithm which is fixed in the "Use .superType instead of .underlying" commit. Using underlying was incorrect as it can change the kind of types, and thus lead to an incorrect conclusion. To paraphrase the example from the commit message, in master it was possible to come to the erroneous conclusion thatf[Dummy]
(for some typef
) andInt
are disjoint. The issue came from the fact that.underlying
onf[Dummy]
returns a type lambda which is kind incompatible withInt
. The fix is to use.superType
which properly preserves kinds.That bug was exploited in
tests/run-macros/tasty-simplified/quoted_2.scala
(from @milessabin) in the Appy/Unapply pattern. I hope this pattern isn't a crucial piece of shapeless, because it breaks with this PR, and (probably) can't be fixed with the current match type semantics.