Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
821 views
in Technique[技术] by (71.8m points)

scala - Ambiguous Reference to overloaded definition - One vs Two Parameters

Given the following companion object with overloaded versions of apply:

object List {
  def apply[T](): List[T] = new Nil
  def apply[T](x1: T): List[T] = new Cons(x1, new Nil)
  def apply[T](x1: T, x2: T): List[T] = new Cons(x1, new Cons(x2, new Nil))
  def apply[T](elems: T*): List[T] = 
    elems.foldRight(List[T])((elem, l) => new Cons(elem, l))
}

And the two instantiations

List(1) // Error - Ambiguity 
List('a', 'b') // Works fine

scalac complains about the first instantiation (ambiguous reference to overloaded definition) because both the single argument and the varargs method are equally specific.

Searching stackoverflow I've found that it is possible to force the single argument method. List[Int](1) will make the compiler use def apply[T](x1: T).

My question is why does the second instantiation match def apply[T](x1: T, x2: T) without extra "hints"? In other words, why is the two argument method more specific than the varargs method where the single argument method isn't?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

To answer your question we need to have a look at what happens when the Scala compiler has to perform overloading resolution. This is described in SLS 6.23.3 (for Scala 2.9).

Let's take a slightly simpler version of your example:

object Test {
  def apply[T](x1: T) = "one arg"                      // A
  def apply[T](x1: T, x2: T) = "two args"              // B
  def apply[T](elems: T*) = "var-args: " + elems.size  // C
}

And look at these three calls:

Test(1) // fails, ambiguous reference, A and C both match arguments
Test[Int](1) // returns "one arg"
Test(1,2) // returns "two args", not "var-args: 2"

Let's start with the first one. First, the compiler looks at the shape of each argument, which is a type that describes, basically, if the argument is a value or a function. Here, no difficulty, 1 is a very normal, boring value and its shape is the type Nothing.

Now it has a single argument 1, of type Nothing, and finds all alternatives that are applicable to it. It finds two of them:

  • apply[T](x1: T): it takes a single argument of unbounded type, so it can receive a argument of type Nothing,
  • apply[T](elems: T*): it can be applied to any number (0 included) of arguments of the same unbounded type, so it can receive a single element of type Nothing.

It there were only one, it would stop there and choose that one.

The second step is identical to the above, except this time it types each argument with an undefined expected type. Basically here it looks at the two alternatives left and finds out if they are applicable to the argument 1 of type A <: Int. No luck, they both are. If you were two change apply[T](x1: T) to apply(x1: String) and leave the other one alone, here there would only be one applicable alternative left and it would succeed and stop.

Then the compiler computes the relative weight of each alternative left over each other. The SLS states that

The relative weight of an alternative A over an alternative B is a number from 0 to 2, defined as the sum of

  • 1 if A is as specific as B , 0 otherwise, and
  • 1 if A is defined in a class or object which is derived from the class or object defining B , 0 otherwise.

At this point, there must be one alternative that has a higher score than all others, or there is an ambiguity error. We can ignore the "defined" part, they are defined in the same place.

  • A is as specific as C because you can always call C with the single argument of A,
  • C is as specific as A because of type inference: you can always call A with the argument of C since A can take anything (its parameter's type can be inferred to whatever we want). C's parameters is seen as a Seq[A] so T is inferred as Seq[A] in A and it can call it. So C is as specific as A.

This can be seen if you change A to apply[T <: Int](x: T): it goes all the way to looking for the most specific one, but this time type inference cannot find a way to make A applicable to C's argument (a Seq) because it isn't a subtype of Int, so A is the most specific. Obviously, the same thing happens if you change A to apply(x: Int), type inference can't even do anything.

This also explains why Test[Int](1) succeeds in calling the one argument version. The two final alternatives are identical, but A's type parameter has been bound to Int and type inference cannot change it to fit C's argument anymore.

Finally, applying the same logic gives you why Test(1,2) works fine:

  • B is as specific as C: you can always call C with B's arguments,
  • but C is not as specific as B: no amount of type inference will manage to fit a single Seq into a method that takes two parameters.

So apply[T](x1: T, x2: T) is the most specific and there are no errors.

Basically for a var-arg and a normal method to produce an ambiguity, you they need to have the same number of arguments and a way to trick type inference on (at least) the last argument:

def apply[T](x1: T)
def apply[T](x: T*)

Or

def apply[T](x1: Int, x2:T)
def apply[T](x1: Int, x: T*)

And so on...

Edit: I wasn't sure at first whether the repeated parameter was seen as a Seq[A] or a TupleX[...] when looking for specificity. It definitely isn't a tuple, and auto-tupling has nothing to do with any of this.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...