Strange behavior of type inference in function with upper bound
Asked Answered
N

3

7

Ran into this strange behavior when changed upper bound in the implementation, but forgot to change it in the interface. I think last statement should not compile, but it does and returns unexpected result.

trait SuperBase
trait Base extends SuperBase

class SuperBaseImpl extends SuperBase

trait Service {
  def doWork[T <: Base : Manifest](body: T => Unit): String
  def print[T <: Base : Manifest]: String
}

object ServiceImpl extends Service {
  override def doWork[T <: SuperBase : Manifest](body: T => Unit): String =
    print[T]
  def print[T <: SuperBase : Manifest]: String =
    manifest[T].runtimeClass.toString
}

val s: Service = ServiceImpl

// does not compile as expected
// s.print[SuperBaseImpl]

// returns "interface Base"
s.doWork { x: SuperBaseImpl => () }

Edit

As @som-snytt mentioned with -Xprint:typer option we can see what compiler actually infers:

s.doWork[Base with SuperBaseImpl]

This explains why we are getting "interface Base". But I still not quite understand how and why type inference work in this case.

Noellanoelle answered 12/11, 2015 at 17:44 Comment(0)
L
1

Note that what your code is saying is:

Method ServeImp.doWork MUST accept a parameter that is "a function that must accept some class T that is a sublass of Base and Superbase"

SuperBaseImpl is not a subclass of Base, but it's not an error because there could exist a class X which "extends SuperBaseImpl with Base" which would satisfy that requirement.

When type inference happens, T is resolved to "foo.Base with foo.SuperBaseImpl" which satisfies all requirements above. runtimeClass is interface Base because there is no way to describe that type in the JVM at runtime, but if you do manifest.toString - you will see the correct type.

There is no real way to demonstrate that with your example, but consider the following:

trait SuperBase
trait Base extends SuperBase

class SuperBaseImpl(val a: String) extends SuperBase

trait Service {
  def doWork[T <: Base : Manifest](body: T => String): (T) => String
}

object ServiceImpl extends Service {
  override def doWork[T <: SuperBase : Manifest](body: T => String): (T) => String =
    x => "Manifest is '%s', body returned '%s'".format(manifest[T].toString(), body(x))
}

val s: Service = ServiceImpl

val f = s.doWork { x: SuperBaseImpl => x.a }
// f: Base with SuperBaseImpl => String = <function1>

f(new SuperBaseImpl("foo") with Base)
// res0: String = Manifest is 'Base with SuperBaseImpl', body returned 'foo'

f(new SuperBaseImpl("foo"))
// compile error 

Here I've made doWork return another function that accepts T and you can see what it resolved to, and that you can actually call that and it will work correctly if you pass something that matches the constraints on all the types.

Added:

Also note that your class hierarchy is not necessary to show that behavior at all, they can be entirely unrelated.

trait A
trait B

def m[T <: A : Manifest](body: T => Unit) = manifest[T].toString()

m((x: B) => Unit)
//res0: String = A with B
Lekishalela answered 18/11, 2015 at 11:44 Comment(0)
D
3

With -Xprint:typer, you'll see what the compiler infers for T:

s.doWork[Base with SuperBaseImpl]

What is the bound trying to express? Functions are co-variant in the parameter, so you are expressing that body must accept a certain arg of a sufficiently narrow type. Normally, you require that a function must deal with a wide type.

Maybe you intended a lower bound.

scala> trait SuperBase
defined trait SuperBase

scala> trait Base extends SuperBase
defined trait Base

scala> class SuperBaseImpl extends SuperBase
defined class SuperBaseImpl

scala> trait Service { def f[A >: Base : Manifest](g: A => Unit): String }
defined trait Service

scala> object Impl extends Service { def f[A >: Base : Manifest](g: A => Unit) = manifest[A].runtimeClass.toString }
defined object Impl

scala> (Impl: Service).f { x: Base => () }
res0: String = interface Base

scala> (Impl: Service).f { x: SuperBase => () }
res1: String = interface SuperBase

scala> (Impl: Service).f { x: SuperBaseImpl => () }
<console>:17: error: inferred type arguments [SuperBaseImpl] do not conform to method f's type parameter bounds [A >: Base]
       (Impl: Service).f { x: SuperBaseImpl => () }
                       ^
<console>:17: error: type mismatch;
 found   : SuperBaseImpl => Unit
 required: A => Unit
       (Impl: Service).f { x: SuperBaseImpl => () }
                                            ^
<console>:17: error: No Manifest available for A.
       (Impl: Service).f { x: SuperBaseImpl => () }
                         ^

scala> object Impl extends Service { def f[A >: SuperBase : Manifest](g: A => Unit) = manifest[A].runtimeClass.toString }
<console>:14: error: overriding method f in trait Service of type [A >: Base](g: A => Unit)(implicit evidence$1: Manifest[A])String;
 method f has incompatible type
       object Impl extends Service { def f[A >: SuperBase : Manifest](g: A => Unit) = manifest[A].runtimeClass.toString }
                                         ^
Doriedorin answered 13/11, 2015 at 6:46 Comment(1)
Thanks for the trick with -Xprint:typer, it makes everything a bit clearer for me. But I still don't understand how and why it works in this way. Is it a bug? About bounds, upper bound was used to call some methods defined in Base/SuperBase before/after calling argument function. And then it was changed in Impl only by mistake.Noellanoelle
R
1

It looks weird but feels sound. Note that you can also call

s.doWork { x: Any => () }

I just think that the type parameter T is somehow "uninhabited". The method cannot know anything about T except its upper bound Base, therefore you get a manifest for Base. But again with that you cannot do much, because that cannot construct a value of type T… So everything remains sound.

Try changing the signature to

def doWork[T <: Base : Manifest](x: T)(body: T => Unit): String

Then you can't use it that way:

s.doWork(123: Int) { x: Any => () }  // no
s.doWork(123: Any) { x: Any => () }  // no
Radarman answered 12/11, 2015 at 21:5 Comment(1)
Thanks, but I think it's easier to just define T explicitly: s.doWork[SuperBaseImpl] { x => () }.Noellanoelle
L
1

Note that what your code is saying is:

Method ServeImp.doWork MUST accept a parameter that is "a function that must accept some class T that is a sublass of Base and Superbase"

SuperBaseImpl is not a subclass of Base, but it's not an error because there could exist a class X which "extends SuperBaseImpl with Base" which would satisfy that requirement.

When type inference happens, T is resolved to "foo.Base with foo.SuperBaseImpl" which satisfies all requirements above. runtimeClass is interface Base because there is no way to describe that type in the JVM at runtime, but if you do manifest.toString - you will see the correct type.

There is no real way to demonstrate that with your example, but consider the following:

trait SuperBase
trait Base extends SuperBase

class SuperBaseImpl(val a: String) extends SuperBase

trait Service {
  def doWork[T <: Base : Manifest](body: T => String): (T) => String
}

object ServiceImpl extends Service {
  override def doWork[T <: SuperBase : Manifest](body: T => String): (T) => String =
    x => "Manifest is '%s', body returned '%s'".format(manifest[T].toString(), body(x))
}

val s: Service = ServiceImpl

val f = s.doWork { x: SuperBaseImpl => x.a }
// f: Base with SuperBaseImpl => String = <function1>

f(new SuperBaseImpl("foo") with Base)
// res0: String = Manifest is 'Base with SuperBaseImpl', body returned 'foo'

f(new SuperBaseImpl("foo"))
// compile error 

Here I've made doWork return another function that accepts T and you can see what it resolved to, and that you can actually call that and it will work correctly if you pass something that matches the constraints on all the types.

Added:

Also note that your class hierarchy is not necessary to show that behavior at all, they can be entirely unrelated.

trait A
trait B

def m[T <: A : Manifest](body: T => Unit) = manifest[T].toString()

m((x: B) => Unit)
//res0: String = A with B
Lekishalela answered 18/11, 2015 at 11:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.