Search code examples
scalaoverloadingthunkbottom-type

Making Scala choose less specific overloaded method in presence of argument of type Nothing


If come across an interesting case with thunks versus functions in the presence of type Nothing:

object Test {
  def apply(thunk: => Any     ): String => Any = _ => thunk
  def apply(fun: String => Any): String => Any = fun
}

val res0 = Test { println("hello") }
res0("foo") // --> hello

val res1 = Test { x: String => println(s"hello $x") }
res1("foo") // --> hello foo

val res2 = Test { x: String => throw new RuntimeException("ex") }
util.Try(res2("foo")) // --> Failure

val res3 = Test { throw new RuntimeException("ex") } // boom!

Now the tricky bit is the last case. Is it possible to modify the apply method to make Scala choose the thunk version of it, instead of the Function1 version (which is more specific and thus preferred, and Nothing <: Function1[_,_], therefore...)

I tried to come up with low-high priority implicits and magnet pattern, but haven't found a solution yet.


Solution

  • Here's a quick draft of a type-safe approach based on type classes:

    object Test {
      trait Wrap[A, B] { def apply(a: => A): String => B }
    
      trait LowWrap {
        implicit def thunkWrap[A] = new Wrap[A, A] { def apply(a: => A) = _ => a }
      }
    
      trait MidWrap extends LowWrap {
        implicit def funcWrap[A] = new Wrap[String => A, A] {
          def apply(f: => String => A) = f
        }
      }
    
      object Wrap extends MidWrap {
        implicit object nothingWrap extends Wrap[Nothing, Nothing] {
          def apply(f: => Nothing) = _ => f
        }
      }
    
      def apply[A, B](a: => A)(implicit w: Wrap[A, B]) = w(a)
    }
    

    And then:

    scala> Test { println("hello") }
    res0: String => Unit = <function1>
    
    scala> res0("foo")
    hello
    
    scala> Test { x: String => println(s"hello $x") }
    res2: String => Unit = <function1>
    
    scala> res2("foo")
    hello foo
    
    scala> Test { x: String => throw new RuntimeException("ex") }
    res4: String => Nothing = <function1>
    
    scala> util.Try(res4("foo"))
    res5: scala.util.Try[Nothing] = Failure(java.lang.RuntimeException: ex)
    
    scala> Test { throw new RuntimeException("ex") }
    res6: String => Nothing = <function1>
    
    scala> util.Try(res6("foo"))
    res7: scala.util.Try[Nothing] = Failure(java.lang.RuntimeException: ex)
    

    You might be able to simplify a bit, add variance, etc.