7

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).

Kevin
  • 2,813
  • 3
  • 20
  • 30

2 Answers2

4

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.

Bogdan Vakulenko
  • 3,380
  • 1
  • 10
  • 25
  • I agree that in this case I would expect that `foo(a, f)` does not compile. I hope somebody can point out why this compiles. – Kevin Apr 03 '19 at 14:13
  • 1
    I guess I have the answer. The reason is type erasure. At the point of pattern matching compiler have only `Sub1[Any, Any]`. And Any =:= Any – Bogdan Vakulenko Apr 03 '19 at 14:27
  • Nah, it has nothing to do with the implicits. You can replace `foo` and `bar` with `def foo[A](a: A, f: A => A)=f(a)`, and still get the same error. Also, `Sub1` is not `[Any, Any]`, but `[Nothing, Nothing]` – Dima Apr 03 '19 at 14:35
  • 1
    @Dima No, it's `[Any, Any]`. You can check that `case Sub1(a, f) => { a: Nothing; f: (Nothing => Nothing); foo(a, f) }` doesn't compile. – Dmytro Mitin Apr 03 '19 at 14:45
  • 3
    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`. – Alexey Romanov Apr 03 '19 at 14:48
  • @AlexeyRomanov the question is why it infers `Any` in the the first case, but not in the second one. What's the difference? – Dima Apr 03 '19 at 14:50
  • @Dima I edited to explain that. It still infers `Any` for `a`, the difference is in `f`. – Alexey Romanov Apr 03 '19 at 14:51
  • 1
    @AlexeyRomanov If I understand well there is only one solution that makes sense: use the generalised type constraints in the `Sub1` and `Sub2` case classes, because when we instantiate such classes the generics are known. And in `foo` and `bar` remove the constraints since they will be called after pattern matching which means the generics are erased and asking for evidence that `Any =:= Any` does not really make sense, and leads to problems for `B => Unit`. Btw, there is still one subtlety I do not get: why is `B => Unit` contravariant in `B` and `B => B` not (since it infers `Any => Any`) ? – Kevin Apr 03 '19 at 18:03
  • @AlexeyRomanov in addition to Kevin's question: what is compiler's logic for this `Nothing => Unit` inference staff. I mean it looks like `Any => Unit` would be fine from type-safety perspective. Is there something that I can pass as types in `Sub2[A, B]` instance that prevents from inferring to `Any => Unit` ? – Bogdan Vakulenko Apr 03 '19 at 18:09
  • 1
    @BogdanVakulenko For the first part, I've added my answer. For "Is there something that I can pass as types in Sub2[A, B] instance that prevents from inferring to Any => Unit", I don't think so. – Alexey Romanov Apr 03 '19 at 18:49
3

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.

Community
  • 1
  • 1
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • This is so instructive! :) What do you mean with "make it work"? When I replace my `dispatch` by yours, I get an error in the first case statement: `type mismatch: expected: Any =:= Any, actual: _ =:= _`. It seems the actual evidence does now contain specific (existential) types `_ =:= _` rather than `Any`s as before. This seems to be what I want but it does not yet compile. For the second case I have a similar error: `expected: Any =:= Nothing, actual: _ =:= _`. – Kevin Apr 03 '19 at 18:59
  • @Kevin It works for me after copying the code from the question and replacing `dispatch`: https://scalafiddle.io/sf/MtEwQai/0. – Alexey Romanov Apr 03 '19 at 19:04
  • Oops my bad, my IDE's type inferencer gives a type mismatch but if I compile it it works :) So, this works because type variable patterns avoid type erasure? I don't fully understand why this is the case/works though. – Kevin Apr 03 '19 at 19:17
  • No, it doesn't avoid type erasure. It just lets the compiler use `a` and `b` in the inferred types, so it doesn't have to reduce to `Any`/`Nothing`. I am not sure I can explain it any better than in the linked question :( – Alexey Romanov Apr 03 '19 at 19:19
  • I forgot to check the linked question, I guess this will make things clear. Thanks a lot :) – Kevin Apr 03 '19 at 19:21
  • @Kevin I probably mislead you a bit with my idea of type erasure. There was no erasure problem in this case at all because implicit evidence is compilation time machinery. At compilation time we still have types and compiler just tying to infer them properly. `Any =:= Any` appears not because types were erased but because they were just inferred to `Any` by compiler. Compiler just could not find better type for this case except `Any`. – Bogdan Vakulenko Apr 03 '19 at 19:35
  • @BogdanVakulenko Indeed, thanks for the clarification. – Kevin Apr 03 '19 at 19:37
  • @AlexeyRomanov I'm still wondering why this code is failing to compile: `case obj @ Sub2(a,f) => bar(a, f)(obj.ev)`. If `B` is inferred to `Nothing` then `obj.ev` should be `Any =:= Nothing` and it should pass type check. – Bogdan Vakulenko Apr 05 '19 at 08:51
  • @BogdanVakulenko Actually, that's a good question, neither am I. I just verified that `obj.ev` is indeed `Any =:= Nothing` by adding `obj.ev: Nothing` and looking at the error message. – Alexey Romanov Apr 05 '19 at 09:07
  • 1
    @BogdanVakulenko And explicitly calling `bar[Any, Nothing](a, f)(obj.ev)` works, it's just refusing to infer `Nothing` here. – Alexey Romanov Apr 05 '19 at 09:09