reducing an Array of Float using scala.math.max

前端 未结 4 1872
温柔的废话
温柔的废话 2021-01-06 09:53

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

相关标签:
4条回答
  • 2021-01-06 10:03

    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.

    0 讨论(0)
  • 2021-01-06 10:08

    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))
    });
    
    0 讨论(0)
  • 2021-01-06 10:09

    There is always scala.math.Ordering:

    Array(1f, 2f, 3f).reduceOption(Ordering.Float.max)
    
    0 讨论(0)
  • 2021-01-06 10:13

    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
    }
    
    0 讨论(0)
提交回复
热议问题