Let's say I have an empty marker trait named Marker
and some functions with type parameters bound by Marker
:
trait Marker
object Marker {
def works[M <: Marker](m:M):M = m
def doesntWork[M <: Marker](f:M => String):String = "doesn't matter"
}
The first function works as I expect. That is, if you pass a parameter which is not a Marker
, then the code does not compile:
scala> works("a string")
<console>:14: error: inferred type arguments [String] do not conform to method works's type parameter bounds [M <: com.joescii.Marker]
works("a string")
^
<console>:14: error: type mismatch;
found : String("a string")
required: M
works("a string")
^
However, I am able to pass a parameter to the second function which does not conform to Marker
. Specifically, I can pass a function of type String => String
and the code happily compiles and runs:
scala> doesntWork( (str:String) => "a string" )
res1: String = doesn't matter
I would expect this call to doesntWork
to fail to compile. Can anyone explain to me why it compiles and how I can change the function signature to prevent the types from checking in such cases?
Full disclosure: the above contrived example is a simplified version of this outstanding issue for lift-ng.
The code compiles because of contravariance. You can see that by giving the inferred type parameter explicitly:
doesntWork[Nothing]((str: String) => "a string")
This is a general problem. There are various techniques to work around that, but they usually boil down to constraining T
to be an instance of some type class.
M => String
is actually a Function1[M, String]
. If you look at the definition:
trait Function1[-T1, +R]
So M
becomes contravariant, which means that for M1 >: M2
, Function1[M1, String] <: Function1[M2, String]
, let's say M1 = Any
then Function1[Any, String] <: Function1[Marker, String]
.
And input of doesntWork
- f
is also contravariant, which means that you can pass something smaller than M => String
, and as I've just shown, Any => String
is smaller than Marker => String
, so it passess completely fine.
You can also pass String => String
because of yours [M <: Marker]
, which finally induces compiler to interpret M
as Nothing
, so even String => String
becomes bigger than M => String
.
To solve your problem, just introduce wrapper, which will make your type invariant:
scala> case class F[M](f: M => String)
defined class F
scala> def doesntWork[M <: Marker](f:F[M]):String = "doesn't matter"
doesntWork: [M <: Marker](f: F[M])String
scala> doesntWork(F((str: String) => "a string"))
<console>:18: error: inferred type arguments [String] do not conform to method doesntWork's type parameter bounds [M <: Marker]
doesntWork(F((str: String) => "a string"))
^
<console>:18: error: type mismatch;
found : F[String]
required: F[M]
doesntWork(F((str: String) => "a string"))
^
scala> doesntWork(F((str: Any) => "a string"))
<console>:18: error: inferred type arguments [Any] do not conform to method doesntWork's type parameter bounds [M <: Marker]
doesntWork(F((str: Any) => "a string"))
^
<console>:18: error: type mismatch;
found : F[Any]
required: F[M]
Note: Any >: M, but class F is invariant in type M.
You may wish to define M as -M instead. (SLS 4.5)
doesntWork(F((str: Any) => "a string"))
scala> doesntWork(F((str: Marker) => "a string"))
res21: String = doesn't matter
scala> trait Marker2 extends Marker
defined trait Marker2
scala> doesntWork(F((str: Marker) => "a string"))
res22: String = doesn't matter
scala> doesntWork(F((str: Marker2) => "a string"))
res23: String = doesn't matter
It's usually bad to recommend such implicit conversions, but seems fine here (if you won't overuse F
):
scala> implicit def wrap[M](f: M => String) = F(f)
warning: there was one feature warning; re-run with -feature for details
wrap: [M](f: M => String)F[M]
scala> doesntWork((str: Marker) => "a string")
res27: String = doesn't matter
scala> doesntWork((str: String) => "a string")
<console>:21: error: inferred type arguments [String] do not conform to method doesntWork's type parameter bounds [M <: Marker]
doesntWork((str: String) => "a string")
^
<console>:21: error: type mismatch;
found : F[String]
required: F[M]
doesntWork((str: String) => "a string")
^
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With