Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

reducing an Array of Float using scala.math.max

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?

like image 436
Austin Avatar asked Dec 25 '22 23:12

Austin


1 Answers

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
}
like image 67
Régis Jean-Gilles Avatar answered Jan 14 '23 13:01

Régis Jean-Gilles