Search code examples
scalatypestypechecking

Why does this Scala function compile when the argument does not conform to the type constraint?


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.


Solution

  • 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")
                                           ^