How does Scala's type inference work with type bounds?
Asked Answered
D

2

6

When type bounds exist on a type parameter, how exactly does Scala determine the type to infer? For example:

def onMouseClicked_=[T >: MouseEvent](lambda: T => Unit) = 
  setOnMouseClicked(new EventHandler[T] {
    override def handle(event: T): Unit = lambda(event)
  })

When attempting to use this function, such as in:

onMouseClicked = { me => doSomething() }

me will have the inferred type of MouseEvent. The type bound of T is a lower type bound, so T must either be of type MouseEvent or a supertype of MouseEvent, so why does me have the inferred type of MouseEvent? Shouldn't it infer the most general type?

Is it something I'm not getting about how Scala's type inference works? Or is my understanding of type bounds completely wrong?

Edit:

Let's say we further restrict the the type of T to be a subtype of Event, where Event is a supertype of MouseEvent. So we get:

def onMouseClicked_=[T >: MouseEvent <: Event](lambda: T => Unit) = 
  setOnMouseClicked(new EventHandler[T] {
    override def handle(event: T): Unit = lambda(event)
  })

So if we do

onMouseClicked = { me: MouseDragEvent => doSomething() }

where MouseDragEvent is a subtype of MouseEvent, the compile fails with a type error, as expected, because the bound ensures that me must be a supertype of MouseEvent.

And yet if we do

onMouseClicked = { me: Any => doSomething() }

the compile succeeds. It's obvious that Any is not a subtype of Event, so why does the compile succeed? What is inferred type of T?

Daphne answered 30/4, 2015 at 11:35 Comment(0)
O
2

I'll give this a try, but I'm not sure everything will be supercorrect or anyway clear.

Premise

The first point to address is that a function from type T to result R (T => R) is covariant in its return type and contravariant in its parameters: i.e. Function[-T, +R].

To make it short it means that for function fsub to be a subtype of function f its return type must be the same type or a subtype of R(covariant) and its parameter must be the same or a supertype of T (contravariant).

Let's try to make sense of this: if you want to be able to use fsub where a f is expected (see Liskov), you need an fsub that can handle at most a passed argument of T, but nothing more specific, because that's what the callers of f are expected to pass to it. Moreover, since a T will be passed, you can be more relaxed on the parameters for your fsub and handle one of its supertypes, since every T passed to it is also an instance of its supertypes.

This is what is meant when we say that a function's argument is contravariant in its type: it tells you how its type can change in relationship subtyping the function as a whole, or actually the other way round.


Given this, let's get back to the specific case. The handler's mutator (onMouseClicked_=) should at least accept a lambda of type MouseEvent => Unit. Let's give it a name, handler: MouseEvent => Unit.

But it doesn't end here, you should expect to be able to pass the method a subtype of this lambda, and what will the type of this "subfunction" be? As we said before it could accept a MouseEvent or any supertype of it, then subhandler: T => Unit with T :> MouseEvent.

And that's exactly the general form of the method you saw.

Conclusion

I hope it became clear now that the reason why the method defines T as a (non-strict) supertype of MouseEvent is not because your handler will ever receive anything different from a MouseEvent, but because you can pass it a lambda that could be able to only handle a more abstract type of event... (e.g. you can use a me: Any => doSomething() )

Outlive answered 2/5, 2015 at 16:12 Comment(2)
Thanks for the great explanation of the first half of my question. What happens when I specify both an upper and lower bound, and pass in a function of Any => Unit, how is T inferred then?Daphne
I guess in the latter case T is inferred as Any, and you can't force it to be different. I suppose that you can't avoid to receive a "handler of any type" in the method, since it's an handler of Events and of MouseEvents too. The only explanation that I can imagine is that you can't force the type system to refuse a subtype of your expected lambda type, since it's just as valid by the rules of subtyping polymophism. Warn: I could be plain wrong here.Outlive
F
1

I'm not entirely confident about the accuracy of this answer, but here goes...

The type of lamba is T => Unit, which is sugar for Function1[T, Unit], which is defined as:

trait Function1[-T1, +R]

T is used for the first parameter T1, which is contravariant, indicated by the - in -T1. For a contravariant parameter, the lowest type in the hierarchy is the most general.

Frankenstein answered 30/4, 2015 at 11:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.