question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

GADT pattern matching unsoundness.

See original GitHub issue
object 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 roles, but I am not sure how applicable it is in the context of Scala).

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:19 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
Blaisorbladecommented, Feb 15, 2018

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”:

  • that paper focuses on newtypes, not abstract types, and newtypes don’t quite behave as abstract types. For instance, they show this code
data K a where
  KAge :: K Age
  KInt :: K Int
get :: K Age→ String
get KAge = "Age"

and claim that

Since get’s type signature declares that its argument is of type K Age, the patterns in get are exhaustive.

But claiming get is exhaustive makes little sense if Age is an abstract type. Instead, get is exhaustive if Age is concrete and distinct from Int, as is the case in Haskell at role “code”. Conversely, the check we’re discussing (looking if there are instantiations of Age that enable matching with KAge) makes little sense in Haskell, since Age 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 at Module because of separate compilation (Module need not exist when we typecheck ModuleUser), 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:

These kinds in turn support the key insight of this paper: it is only safe to lift coercions through functions with parametric kinds.

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.

  • in the end, Haskell doesn’t implement roles as in “​Generative Type Abstraction and Type-level Computation”, but uses the evolved variant in “Safe Zero-cost Coercions for Haskell”; not sure if that complicates the picture, I might have to check it.
1reaction
Blaisorbladecommented, Feb 15, 2018

It’s either that, or admit that we don’t know if abstract type members are subtypes of other types.

👍 for the abstract types idea.

Hm, on the other hand they are not really abstract (path.T for a stable path)

path is stable so T has a fixed definition, but that definition is hidden so path.T is still abstract. You can turn the Module 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 to A' <:< B' (well, childTypeMap.apply(parent) <:< parentTypeMap.apply(tp2)) while preserving the result. It tries to compute A' and B' by picking a suitable substitution for type variables, but (a) in our scenario, we care about abstract type Foo 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 that G(A) <:< G(B), and in this case there is indeed such a substitution (G(Foo) = Int). Not sure how to find it in general; but K[Foo] <:< K[Int] reduces to Foo =:= 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 match K[Foo] against K1 seems equivalent to trying to apply def foobar[Foo](arg: F[Foo]) to F[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.

trait ModuleSig {
  type F[_]
  type U

  trait FooSig {
    type Type = F[U]
    def subst[F[_]](fa: F[Int]): F[Type]
  }

  val Foo: FooSig
}

class ModuleUser(Module: ModuleSig) {
  def foo(): Unit = {
    type Foo = Module.Foo.Type

    sealed abstract class K[F]
    final case object K1 extends K[Int]
    final case object K2 extends K[Foo]

    val kv: K[Foo] = Module.Foo.subst[K](K1)
    def test(k: K[Foo]): Unit = k match {
      case K2 => ()
    }

    test(kv)
  }
}

object App {
  val Module: ModuleSig = new ModuleSig {
    type F[A] = Int

    val Foo: FooSig = new FooSig {
      // type Type = Int
      def subst[F[_]](fa: F[Int]): F[Type] = fa
    }
  }
  def main(args: Array[String]): Unit =
    new ModuleUser(Module).foo()
}
Read more comments on GitHub >

github_iconTop 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 >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found