reducing an Array of Float using scala.math.max

scala, scala-2.10

Solution

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
}

Problem

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?

Original source