I am confused by the following behavior - why does reducing an Array of Int work using math.max, but an Array of Float requires a wrapped function? I have memories that this was not an issue in 2.9, but I'm not completely certain about that.
$ scala -version
Scala code runner version 2.10.2 -- Copyright 2002-2013, LAMP/EPFL
$ scala
scala> import scala.math._
scala> Array(1, 2, 4).reduce(max)
res47: Int = 4
scala> Array(1f, 3f, 4f).reduce(max)
<console>:12: error: type mismatch;
found : (Int, Int) => Int
required: (AnyVal, AnyVal) => AnyVal
Array(1f, 3f, 4f).reduce(max)
^
scala> def fmax(a: Float, b: Float) = max(a, b)
fmax: (a: Float, b: Float)Float
scala> Array(1f, 3f, 4f).reduce(fmax)
res45: Float = 4.0
update : this does work
scala> Array(1f, 2f, 3f).reduce{(x,y) => math.max(x,y)}
res2: Float = 3.0
so then it is just reduce(math.max)
which cannot be shorthanded?
The first thing to note is that math.max
is overloaded, and if the compiler has no hint about the expected argument types, it just picks one of the overloads (I'm not clear yet on what rules govern which overload is picked, but it will become clear before the end of this post).
Apparently it favors the overload that takes Int
parameters over the others. This can be seen in the repl:
scala> math.max _
res6: (Int, Int) => Int = <function2>
That method is most specific because the first of the following compiles (by virtue of numeric widening conversions) and the second does not:
scala> (math.max: (Float,Float)=>Float)(1,2)
res0: Float = 2.0
scala> (math.max: (Int,Int)=>Int)(1f,2f)
<console>:8: error: type mismatch;
found : Float(1.0)
required: Int
(math.max: (Int,Int)=>Int)(1f,2f)
^
The test is whether one function applies to the param types of the other, and that test includes any conversions.
Now, the question is: why can't the compiler infer the correct expected type? It certainly knows that the type of Array(1f, 3f, 4f)
is Array[Float]
We can get a clue if we replace reduce
with reduceLeft
: then it compiles fine.
So surely this has to do with a difference in the signature of reduceLeft
and reduce
.
We can reproduce the error with the following code snippet:
case class MyCollection[A]() {
def reduce[B >: A](op: (B, B) => B): B = ???
def reduceLeft[B >: A](op: (B, A) => B): B = ???
}
MyCollection[Float]().reduce(max) // Fails to compile
MyCollection[Float]().reduceLeft(max) // Compiles fine
The signatures are subtly different.
In reduceLeft
the second argument is forced to A
(the collection's type), so type inference is trivial: if A==Float (which the compiler knows), then the compiler knows that the only valid overload of max
is one that takes a Float
as its second argument. The compiler only finds one ( max(Float,Float)
), and it happens that the other constraint (that B >: A
) is trivially satisfied (as B == A == Float
for this overload).
This is different for reduce
: both the first and second arguments can be any (same) super-type of A
(that is, of Float
in our specific case). This is a much more lax constraint, and while it could be argued that in this case the compiler could see that there is only one possibility, the compiler is not smart enough here.
Whether the compiler is supposed to be able to handle this case (meaning that this is an inference bug) or not, I must say I don't know. Type inference is a tricky business in scala, and as far as I know the spec is intentionally vague about what can be inferred or not.
Since there are useful applications such as:
scala> Array(1f,2f,3f).reduce[Any](_.toString+","+_.toString)
res3: Any = 1.0,2.0,3.0
trying overload resolution against every possible substitution of the type parameter is expensive and could change the result depending on the expected type you wind up with; or would it have to issue an ambiguity error?
Using -Xlog-implicits -Yinfer-debug
shows the difference between reduce(math.max)
, where overload resolution happens first, and the version where the param type is solved for first:
scala> Array(1f,2f,3f).reduce(math.max(_,_))
[solve types] solving for A1 in ?A1
inferExprInstance {
tree scala.this.Predef.floatArrayOps(scala.Array.apply(1.0, 2.0, 3.0)).reduce[A1]
tree.tpe (op: (A1, A1) => A1)A1
tparams type A1
pt ?
targs Float
tvars =?Float
}
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