Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern matching on classes containing generalized type constraints

Tags:

scala

I have a trait that is extended by multiple subclasses

trait Sup
case class Sub[A, B](a: A, f: B => B)(implicit val ev: A =:= B) extends Sup
case class Sub2[A, B](a: A, f: B => Unit)(implicit val ev: A =:= B) extends Sup

And two functions:

def foo[A, B](a: A, f: B => B)(implicit ev: A =:= B) = f(a)
def bar[A, B](a: A, f: B => Unit)(implicit ev: A =:= B) = f(a)

Now I can do some form of dynamic dispatching and call foo if the object is a Sub and bar if the object is a Sub2.

def dispatch(obj: Sup) = {
    obj match {
      case Sub(a, f) => foo(a, f)
      case Sub2(a, f) => bar(a, f) // type mismatch: found: Nothing => Unit. required: B => Unit
    }
  }

I've also tried to pass the evidence explicitly but it results in the same error:

case o @ Sub2(a, f) => bar(a, f)(o.ev) // type mismatch

It is very weird that f: B => B works (I can call foo), but f: B => Unit doesn't work (I can't call bar).

like image 760
Kevin Avatar asked Apr 03 '19 12:04

Kevin


2 Answers

Not an answer but something to think about:

case class Sub1[A, B](a: A, f: B => B)
case class Sub2[A, B](a: A, f: B => Unit)

def foo[A, B](a: A, f: B => B)(implicit ev: A =:= B) = f(a)
def bar[A, B](a: A, f: B => Unit)(implicit ev: A =:= B) = f(a)

def dispatch(obj: Any) = obj match {
    case Sub1(a, f) => foo(a, f)
    case Sub2(a, f) => bar(a, f) // type mismatch: found: Nothing => Unit. required: B => Unit
}

This code have the same problem as yours but Sub1 and Sub2 case classes don't even have implicit blocks.

implicit section in case class doesn't effect pattern resolution. This section is just syntax sugar for calling apply(a: A, f: B => B)(implicit val ev: A =:= B) method on Sub1/2's companion objects. Pattern matching use unapply method to match the pattern at runtime and this unapply don't even know about evidences.

But I'm still wondering why first case is compiled without having this evidence.

Edit: Adding helpful comment from @AlexeyRomanov

More type inference than type erasure. But yes, the compiler infers type Any for a and Any => Any for f and then produces and uses evidence that Any =:= Any. In the second case it infers Nothing => Unit for f, because B => Unit is contravariant in B, and fails to find Any =:= Nothing.

like image 68
Bogdan Vakulenko Avatar answered Nov 11 '22 00:11

Bogdan Vakulenko


You can actually make it work using type variable patterns:

def dispatch(obj: Sup) = {
    obj match {
      case obj: Sub[a, b] => foo(obj.a, obj.f)(obj.ev)
      case obj: Sub2[a, b] => bar(obj.a, obj.f)(obj.ev)
    }
  }

This part is an answer to the comments, because it doesn't really fit in there:

Btw, there is still one subtlety I do not get: why is B => Unit contravariant in B

what is compiler's logic for this Nothing => Unit inference staff

You need to start with function variance. X => Y is a subtype of X1 => Y1 if and only if X is a supertype of X1 and Y is a subtype of Y1. We say it's contravariant in X and covariant in Y.

So if you fix Y = Unit, what remains is just contravariant in X. Any => Unit is a subtype of String => Unit, which is a subtype of Nothing => Unit. In fact, Nothing => Unit is the most general of all B => Unit, and that's why it gets inferred in the Sub2 case.

and B => B not (since it infers Any => Any) ?

The situation with B => B is different: String => String is neither a subtype nor a supertype of Any => Any, or of Nothing => Nothing. That is, B => B is invariant. So there is no principled reason to infer any specific B, and in this case the compiler uses the upper bound for B (Any), and B => B becomes Any => Any.

like image 33
Alexey Romanov Avatar answered Nov 10 '22 22:11

Alexey Romanov