Search code examples
scalaimplicitscala-3match-types

Proving that a match type resolves to a specific concrete type


I am trying to create an implementation of a trait that uses a match type, where the right-hand-side of that match type is known in advance. However, I can't seem to get the compiler to accept my "proof" of this. This is pretty new to me, so sorry if this is really obvious. Can someone help me understand if/how I can achieve what I want?

Here's some minimal code (Scastie) to illustrate what I'm doing:

class NumberBox {}

class LongBox {}

trait Boxer[T] {
  def box(): Boxer.Box[T]
}

object Boxer {
  type Box[T] = T match
    case Long => LongBox
    case _ => NumberBox
}

case class Val[T](v: T) extends Boxer[T] {
  def box(): Boxer.Box[T] = v match
    case _: Long => new LongBox()
    case _ => new NumberBox()
}

// here we prove that Boxer.Box[T] is a NumberBox
case class NoLongs[T](v: Boxer[T])(using Boxer.Box[T] =:= NumberBox) extends Boxer[T] {
  override def box(): Boxer.Box[T] = new NumberBox()
}

NoLongs(Val(1))

But this fails with:

Found:    NumberBox
Required: Boxer.Box[T]

Note: a match type could not be fully reduced:

  trying to reduce  Boxer.Box[T]
  failed since selector  T
  does not match  case Long => LongBox
  and cannot be shown to be disjoint from it either.
  Therefore, reduction cannot advance to the remaining case

    case _ => NumberBox
    override def box(): Boxer.Box[T] = new NumberBox()

Solution

  • Scala doesn't automatically apply proofs in the way you're looking for. But you're right that you do have a proof. In fact, your proof constitutes a callable object: the type =:=[From, To] defines a method called apply:

    override def apply(f: From): To
    

    Now, you have a value of type Boxer.Box[T] =:= NumberBox, which means that apply would convert a Boxer.Box[T] to a NumberBox. You want the opposite: to convert a NumberBox into a Boxer.Box[T]. So we need to flip your proof around and then apply it.

    def flip: To =:= From
    

    In your specific use case, consider

    case class NoLongs[T](v: Boxer[T])(using eq: Boxer.Box[T] =:= NumberBox) extends Boxer[T] {
      override def box(): Boxer.Box[T] = eq.flip(new NumberBox())
    }
    

    We give the proof argument a name, eq, and then we apply its symmetric proof (that NumberBox =:= Boxer.Box[T]) to our NumberBox to convert it to the desired type.

    You could also just take a proof that NumberBox =:= Boxer.Box[T] directly and get rid of the flip, if desired.

    case class NoLongs[T](v: Boxer[T])(using eq: NumberBox =:= Boxer.Box[T]) extends Boxer[T] {
      override def box(): Boxer.Box[T] = eq(new NumberBox())
    }