-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Verify HKT GADT code #9044
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
Comments
Hmm sorry, this is about GADT, not about exhaustivity checking, there might be some exhaustivity checking issue but they're not what's preventing the initial error. |
@smarter seems like letting GADT refinements be used for HKTs didn't go so well. Do you suppose there's anything I could read on how subtyping is supposed to work for HKTs in presence of variance before I fiddle with that code again? |
Like a write-up or paper? I don't think that exists. |
I don't think variance was treated in Sandro's thesis (turns out things are already really complicated without it!) |
No, unfortunately I did not manage to include variances in my thesis. Adding them is WIP but turned out to be much harder than I thought. The issue is that having variances, currying and dependent types in the same calculus destroys some nice properties that one normally uses to model such theories. For the time being, it's an open problem to combine these, to the best of my knowledge. For many instances, however, we don't need to worry about dependent kinds (in particular if no type bounds are involved). And for that case, we have a theory. See for example the following paper by Andreas Abel (you can safely ignore the bit about sized types if you're only interested in variances and HK types):
|
Hum, the interplay between variances and refinement here hurts my head. I'm honestly not sure how this would be desugared into any formal system I know of. But I think @smarter is right that the 0.24.0-RC1 behavior is correct, because |
If you give me a few days, I might be able to get back to you with a more definitive answer. In the meantime, read Abel's paper y'all. It's really nice! |
OK, I tried to break down the example into more elementary terms — something closer to a formal calculus — to make it more obvious (to me at least) where things should or shouldn't go wrong. I'm starting from the following simplified example: object Test {
sealed trait Test[+F[_], +A] extends Product with Serializable
final case class Completed[F[_], A](fa: F[A]) extends Test[F, A]
def foo[F[_], A, B](x: Test[F, A], completed: F[A] => B): B = x match {
case Completed(fa) => completed(fa)
}
} When I run this in scastie I get the 0.24.0-RC1 error. (Aside: what's the easiest way to get an up-to-date dotty shell on MacOS these days? Is there a brew package?) Now let's try to translate this into something DOT like: /* Define the interface first... */
trait TestIntf {
trait Test[+F[_], +A] { // should be type `type Test[+F[_], +A] <: ...`
type CompletedF[X] <: F[X] // but dotty doesn't like structural records.
type CompletedA <: A
def elim[B](f: CompletedF[CompletedA] => B): B
}
type Completed[F[_], A] <: Test[F, A];
}
/* ... then, separately, the implementation. */
object TestImpl extends TestIntf {
type Completed[F[_], A] = Test[F, A] { // should be `... = Test[F, A] & ...`
type CompletedF[X] = F[X]
type CompletedA = A
def elim[B](f: CompletedF[CompletedA] => B): B
}
def Completed[F[_], A](fa: F[A]): Completed[F, A] = new Test[F, A] {
type CompletedF[X] = F[X]
type CompletedA = A
def elim[B](f: CompletedF[CompletedA] => B): B = f(fa)
}
def foo[F[_], A, B](x: Test[F, A], completed: F[A] => B): B = x.elim {
fa => completed(fa)
}
} Note that I separated interface from implementation so that we can see what the constraints are that we set up and where the implementation breaks those. I tried to get rid of pattern matching, traits and refinement, but unfortunately, dotty won't quite accept my bare-bones (HK-)DOT code because it doesn't like raw structural records (at least not with polymorphic methods in it). This is why Note that there is no pattern matching in this code. Instead, there is an abstract The implementation is then quite straightforward: we fix a concrete representation for The crucial bit is the implementation of I don't know whether this clarifies much. I guess the more low-level version exposes some tacit assumptions that are not plainly visible in the original. For example that there are constraints between the type parameters of Let me know if this helped. |
@sstucki thanks for the detailed rundown. AFAIK your understanding of GADTs in terms of type members refined during pattern matching is precisely how the compiler is supposed to reason about them — see also our short paper at the Scala symposium (Towards Improved GADT Reasoning in Scala). So this reasoning was apparently made buggy by recent changes in constraints representation/handling. |
Excellent, thanks for the pointer! |
A patchwork fix was applied in #9140. It turned out that the culprit was GADT member lookup healing + Dotty idiosyncracies. Still, I'm leaving this issue open, since I do want to take a look into the original code at some point and figure out whether it's sound or not. @sstucki thanks for your detailed answer! As Lionel said, this looks close to how I tried to think about GADTs and it definitely helps to have confirmation from another angle that this kind of thinking makes sense. |
Minimized code
Output
Dotty 0.24.0-RC1:
Dotty master:
Expectation
I think 0.24.0-RC1 got it right, current master is missing the error and producing some weird warnings.
The text was updated successfully, but these errors were encountered: