Skip to content
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

Trait with type parameter does not specialize match result type #12475

Open
atennapel opened this issue May 14, 2021 · 6 comments
Open

Trait with type parameter does not specialize match result type #12475

atennapel opened this issue May 14, 2021 · 6 comments
Assignees

Comments

@atennapel
Copy link

atennapel commented May 14, 2021

Compiler version

3.0

Minimized code

sealed trait Ty {
  type T
}
case object TUnit extends Ty {
  type T = Unit
}
final case class TFun(dom: Ty, cod: Ty) extends Ty {
  type T = dom.T => cod.T
}

def default(ty: Ty): ty.T = ty match {
  case TUnit => ()
  case TFun(dom, cod) => { (x: dom.T) => default(cod) }
}

Output

Found:    Unit
Required: ty.T

Found:    dom.T => cod.T
Required: ty.T

Expectation

I expected the result type inside of the match to be specialized to Unit for TUnit and dom.T => cod.T for TFun, but this is not the case. It may be the case that I expect too much though.

With GADTs the result types are specialized, but I cannot type the x in the case of TFun, because I have no access to the type parameters of TFun:

sealed trait Ty[T]
case object TUnit extends Ty[Unit]
final case class TFun[A, B](dom: Ty[A], cod: Ty[B]) extends Ty[A => B]

def default[T](ty: Ty[T]): T = ty match {
  case TUnit => ()
  case TFun(dom, cod) => { x => default(cod) }
  /*
  Missing parameter type

  I could not infer the type of the parameter x.
  */
}
@LPTK
Copy link
Contributor

LPTK commented May 14, 2021

GADT reasoning does not yet look at type members AFAIK. Here is an ugly workaround that wrangles the proofs manually:

sealed trait Ty {
  type T
}
class TUnit() extends Ty {
  type T = Unit
}
case object TUnit extends TUnit()
final case class TFun(dom: Ty, cod: Ty) extends Ty {
  type T = dom.T => cod.T
}
def default(ty: Ty): ty.T = ty match {
  case ty: (ty.type & TUnit) => (): ty.T
  case ty: (ty.type & TFun) =>
    val f = { (x: ty.dom.T) => default(ty.cod) }
    f: ty.T
}

The reason I had to make TUnit a class is that Dotty has limitations when it comes to intersections of singleton types.

However, this strangely raises a warning:

match may not be exhaustive.
It would fail on pattern case: TUnit(), TFun(_, _)

Maybe @liufengyun can tell us why.


Going back to the type-parameter version: In principle, you should be able to write:

def default[T](ty: Ty[T]): T = ty match {
  case TUnit => ()
  case TFun(dom, cod): TFun[a, b] => { (x: a) => default(cod) }
}

but it does not work:

Found:    a => b
Required: T

Maybe @abgruszecki has an idea why.

@liufengyun
Copy link
Contributor

liufengyun commented May 17, 2021

However, this strangely raises a warning:

match may not be exhaustive.
It would fail on pattern case: TUnit(), TFun(_, _)

Maybe @liufengyun can tell us why.

The reason is that the exhaustivity check always widens the singleton type of selector first -- after widening the subtyping does not hold any more.

@LPTK
Copy link
Contributor

LPTK commented May 17, 2021

@liufengyun I seem to remember that this used to be different, wasn't it? It would be nice if there was at least a special-case to allow refining what is matched to subtype the singleton type of the scrutinee, otherwise we lose too much information in type-heavy programming patterns.

@liufengyun
Copy link
Contributor

@liufengyun I seem to remember that this used to be different, wasn't it? It would be nice if there was at least a special-case to allow refining what is matched to subtype the singleton type of the scrutinee, otherwise we lose too much information in type-heavy programming patterns.

I think it's possible to do better here. I'll give it a try.

@liufengyun
Copy link
Contributor

liufengyun commented May 20, 2021

@LPTK I proposed a tentative solution in #12549 -- however, the scrutinee has to be explicitly annotated:

sealed trait Ty {
  type T
}

class TUnit() extends Ty {
  type T = Unit
}

case object TUnit extends TUnit()

final case class TFun(dom: Ty, cod: Ty) extends Ty {
  type T = dom.T => cod.T
}

def default(ty: Ty): ty.T = (ty: ty.type & Ty) match {
  case a: (ty.type & TUnit) => (): a.T
  case a: (ty.type & TFun) =>
    val f = { (x: a.dom.T) => default(a.cod) }
    f: a.T
}

@LPTK
Copy link
Contributor

LPTK commented May 26, 2021

@liufengyun Cool, thanks for taking the time to look into this!

liufengyun added a commit to dotty-staging/dotty that referenced this issue Jun 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants