views:

380

answers:

5

Can anyone explain the compile error below? Interestingly, if I change the return type of the get() method to String, the code compiles just fine. Note that the thenReturn method has two overloads: a unary method and a varargs method that takes at least one argument. It seems to me that if the invocation is ambiguous here, then it would always be ambiguous.

More importantly, is there any way to resolve the ambiguity?

import org.scalatest.mock.MockitoSugar
import org.mockito.Mockito._

trait Thing { 
   def get(): java.lang.Object 
}

new MockitoSugar {   
   val t = mock[Thing]  

   when(t.get()).thenReturn("a")  
}

error: ambiguous reference to overloaded definition, both method thenReturn in trait OngoingStubbing of type
java.lang.Object,java.lang.Object*)org.mockito.stubbing.OngoingStubbing[java.lang.Object] and method thenReturn in trait OngoingStubbing of type (java.lang.Object)org.mockito.stubbing.OngoingStubbing[java.lang.Object] match argument types (java.lang.String) when(t.get()).thenReturn("a")

+1  A: 

Well, I figured out how to resolve the ambiguity (seems kind of obvious in retrospect):

when(t.get()).thenReturn("a", Array[Object](): _*)

As Andreas noted, if the ambiguous method requires a null reference rather than an empty array, you can use something like

v.overloadedMethod(arg0, null.asInstanceOf[Array[Object]]: _*)

to resolve the ambiguity.

Aaron Novstrup
+1  A: 

Well, it is ambiguous. I suppose Java semantics allow for it, and it might merit a ticket asking for Java semantics to be applied in Scala.

The source of the ambiguitity is this: a vararg parameter may receive any number of arguments, including 0. So, when you write thenReturn("a"), do you mean to call the thenReturn which receives a single argument, or do you mean to call the thenReturn that receives one object plus a vararg, passing 0 arguments to the vararg?

Now, what this kind of thing happens, Scala tries to find which method is "more specific". Anyone interested in the details should look up that in Scala's specification, but here is the explanation of what happens in this particular case:

object t {
  def f(x: AnyRef) = 1 // A
  def f(x: AnyRef, xs: AnyRef*) = 2 // B
}

if you call f("foo"), both A and B are applicable. Which one is more specific?

  • it is possible to call B with parameters of type (AnyRef), so A is as specific as B.
  • it is possible to call A with parameters of type (AnyRef, Seq[AnyRef]) thanks to tuple conversion, Tuple2[AnyRef, Seq[AnyRef]] conforms to AnyRef. So B is as specific as A. Since both are as specific as the other, the reference to f is ambiguous.

As to the "tuple conversion" thing, it is one of the most obscure syntactic sugars of Scala. If you make a call f(a, b), where a and b have types A and B, and there is no f accepting (A, B) but there is an f which accepts (Tuple2(A, B)), then the parameters (a, b) will be converted into a tuple.

For example:

scala> def f(t: Tuple2[Int, Int]) = t._1 + t._2
f: (t: (Int, Int))Int

scala> f(1,2)
res0: Int = 3

Now, there is no tuple conversion going on when thenReturn("a") is called. That is not the problem. The problem is that, given that tuple conversion is possible, neither version of thenReturn is more specific, because any parameter passed to one could be passed to the other as well.

Daniel
+1  A: 

If you look at the standard library APIs you'll see this issue handled like this:

def meth(t1: Thing): OtherThing = { ... }
def meth(t1: Thing, t2: Thing, ts: Thing*): OtherThing = { ... }

By doing this, no call (with at least one Thing parameter) is ambiguous without extra fluff like Array[Thing](): _*.

Randall Schulz
That does seem like a better way to write it. Unfortunately, `thenReturn` is defined in a third-party Java library (Mockito). As Daniel pointed out Java resolves the ambiguity to the non-varargs method, so I can't even call it a bug in the library.
Aaron Novstrup
And it is quite likely to be vulnerable to ambiguity when passing two parameters, now that I understand the issue.
Daniel
+1  A: 

Hi. I had a similar problem using Oval (oval.sf.net) trying to call it's validate()-method.

Oval defines 2 validate() methods:

public List<ConstraintViolation> validate(final Object validatedObject)
public List<ConstraintViolation> validate(final Object validatedObject, final String... profiles)

Trying this from Scala: validator.validate(value) produces the following compiler-error:

both method validate in class Validator of type (x$1: Any,x$2: <repeated...>[java.lang.String])java.util.List[net.sf.oval.ConstraintViolation]                                                          
and  method validate in class Validator of type (x$1: Any)java.util.List[net.sf.oval.ConstraintViolation]                                                                                               
match argument types (T)                                                                                                                                                                                
        var violations = validator.validate(entity);                                                                                                                                                    

Oval needs the varargs-parameter to be null, not an empty-array, so I finally got it to work with this:

validator.validate(value, null.asInstanceOf[Array[String]]: _*)

Andreas
Thanks for adding your case, Andreas. I documented it on Daniel's trac ticket (https://lampsvn.epfl.ch/trac/scala/ticket/2991) as well.
Aaron Novstrup
+1  A: 

In the specific case of Mockito, it's possible to use the alternate API methods designed for use with void methods:

doReturn("a").when(t).get()

Clunky, but it'll have to do, as Martin et al don't seem likely to compromise Scala in order to support Java's varargs.

Coda Hale