GADT pattern matching unsoundness.
See original GitHub issueobject App {
def main(args: Array[String]): Unit = {
trait AgeT {
type T
def subst[F[_]](fa: F[Int]): F[T]
}
type Age = Age.T
final val Age: AgeT = new AgeT {
type T = Int
def subst[F[_]](fa: F[Int]): F[T] = fa
}
sealed abstract class K[A]
final case object KAge extends K[Age]
final case object KInt extends K[Int]
val kint: K[Age] = Age.subst[K](KInt)
def get(k: K[Age]): String = k match {
case KAge => "Age"
}
get(kint)
}
}
Produces no warnings but results in a runtime MatchError
failure.
Somewhat relevant paper (it describes a similar problem in Haskell that was solved by the introduction of role
s, but I am not sure how applicable it is in the context of Scala).
Issue Analytics
- State:
- Created 6 years ago
- Comments:19 (10 by maintainers)
Top Results From Across the Web
Pattern-Matching Warnings That Account for GADTs, Guards ...
PDF | For ML and Haskell, accurate warnings when a function definition has redundant or missing patterns are mission critical.
Read more >Theoretical Foundations for Objects With Pattern Matching ...
A case for DOT: Theoretical Foundations for Objects With Pattern Matching and GADT-style Reasoning. Authors:Aleksander Boruch-Gruszecki, ...
Read more >Complete and Decidable Type Inference for GADTs - Microsoft
The key difficulty is that a GADT pattern match brings local type ... order to solve such a constraint at the end, it...
Read more >A case for DOT: theoretical foundations for objects with pattern ...
But pattern matching on generic class hierarchies currently results in puzzling ... a classical constraint-based GADT calculus, into cDOT.
Read more >Understanding the limits of Scala GADT support - Stack Overflow
It seems to me that the class type parameters can easily be made less precise by upcasting, and as long as pattern matching...
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
I’m looking at roles, but I’m still wondering if they’re indeed applicable to our Scala scenario — I haven’t decided but I still think not. Maybe I’m missing something?
Looking at “Generative Type Abstraction and Type-level Computation”:
and claim that
But claiming
get
is exhaustive makes little sense ifAge
is an abstract type. Instead,get
is exhaustive ifAge
is concrete and distinct fromInt
, as is the case in Haskell at role “code”. Conversely, the check we’re discussing (looking if there are instantiations ofAge
that enable matching withKAge
) makes little sense in Haskell, sinceAge
isn’t a type variable.Overall, the goal is to take some hidden type equalities, and make them visible at role “type” (Sec. 3). But when typechecking
ModuleUser
we can’t look atModule
because of separate compilation (Module
need not exist when we typecheckModuleUser
), so that idea doesn’t translate so easily to our context. Sec. 1 claims the problem could arise with ML-style module systems, which are closer to Scala, and points to Sec. 7, but that doesn’t show how the issues could arise with abstract types.They add in Sec. 3.2 that:
but since Scala has abstract types, we can soundly write the coercions we want ourselves (as you did), and they’re sound by themselves, it’s hard to blame the coercion lifting, and much easier to blame the match.
👍 for the abstract types idea.
path
is stable soT
has a fixed definition, but that definition is hidden sopath.T
is still abstract. You can turn theModule
into an argument so that it’s more clearly abstract — I’ve done the exercise below.I only have a vague idea about the code in #3646, but your intuition about what quantification is needed makes sense. IIUC, the code tries to reduce
A <:< B
toA' <:< B'
(well,childTypeMap.apply(parent) <:< parentTypeMap.apply(tp2)
) while preserving the result. It tries to computeA'
andB'
by picking a suitable substitution for type variables, but (a) in our scenario, we care about abstract typeFoo
in invariant position, so there’s not much to maximize/minimize (b) the question we care for is "is there a type substitution G such thatG(A) <:< G(B)
, and in this case there is indeed such a substitution (G(Foo) = Int
). Not sure how to find it in general; butK[Foo] <:< K[Int]
reduces toFoo =:= Int
by invariance, which can be solved by unification (well, even matching). Unification and/or matching with subtyping are probably trickier, but we need some version of it for type inference? EDIT: in fact, in this case trying to matchK[Foo]
againstK1
seems equivalent to trying to applydef foobar[Foo](arg: F[Foo])
toF[Int]
: in both cases we end up searching for the same type substitution.@liufengyun any chance we can talk about this (maybe even tomorrow)? I’m sure I don’t fully get the code but maybe we can help.