reducing an Array of Float using scala.math.max

二次信任 提交于 2019-12-19 03:31:09

问题


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?


回答1:


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
}



回答2:


It looks like this is a bug in the inferrer, cause with Int it infers types correctly:

private[this] val res2: Int = scala.this.Predef.intArrayOps(scala.Array.apply(1, 2, 4)).reduce[Int]({
  ((x: Int, y: Int) => scala.math.`package`.max(x, y))
});

but with Floats:

private[this] val res1: AnyVal = scala.this.Predef.floatArrayOps(scala.Array.apply(1.0, 3.0, 4.0)).reduce[AnyVal]({
  ((x: Int, y: Int) => scala.math.`package`.max(x, y))
});

If you explicitly annotate reduce with a Float type it should work:

Array(1f, 3f, 4f).reduce[Float](max)

private[this] val res3: Float = scala.this.Predef.floatArrayOps(scala.Array.apply(1.0, 3.0, 4.0)).reduce[Float]({
  ((x: Float, y: Float) => scala.math.`package`.max(x, y))
});



回答3:


There is always scala.math.Ordering:

Array(1f, 2f, 3f).reduceOption(Ordering.Float.max)



回答4:


It doesn't seem to be a bug. Consider the following code:

class C1 {}

object C1 {
  implicit def c2toc1(x: C2): C1 = new C1
}

class C2 {}

class C3 {
  def f(x: C1): Int = 1
  def f(x: C2): Int = 2
}

(new C3).f _                                    //> ... .C2 => Int = <function1>

If I remove implicit conversion I will get an error "ambiguous reference". And because Int has an implicit conversion to Float Scala tries to find the most specific type for min, which is (Int, Int) => Int. The closest common superclass for Int and Float is AnyVal, that's why you see (AnyVal, AnyVal) => AnyVal.

The reason why (x, y) => min(x, y) works is probably because eta-expansion is done before type inference and reduce has to deal with (Int, Int) => Int which will be converted to (AnyVal, AnyVal) => AnyVal.

UPDATE: Meanwhile (new C3).f(_) will fail with "missing parameter type" error, which means f(_) depends on type inference and doesn't consider implicit conversions while f _ doesn't need parameter type and will expand to the most specific argument type if Scala can find one.



来源:https://stackoverflow.com/questions/18108965/reducing-an-array-of-float-using-scala-math-max

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!