3

Continuing my series about strange source code.

Looking at Scala 2.12.12 scala.collection.TraversableOnce#reduceLeft#reducer I've found a very strange line:

def reduceLeft[B >: A](op: (B, A) => B): B = {
  if (isEmpty)
    throw new UnsupportedOperationException("empty.reduceLeft")

  object reducer extends Function1[A, Unit] {
    var first = true
    var acc: B = 0.asInstanceOf[B] // <<<<===

    override def apply(x: A): Unit =
      if (first) {
        acc = x
        first = false
      }
      else acc = op(acc, x)
  }
  self foreach reducer
  reducer.acc
}

What actually 0.asInstanceOf[B] does? Is it a workaround for making each type "nullable"?

For example, having

Seq("1", "2").reduceLeft(_ + _)

means the following code in the runtime

var acc: B = 0.asInstanceOf[String]

Why this cannot be simply replaced with var acc: B = null? Because it would require to introduce implicit ev: Null <:< A1 or what?

Update:

Moreover, simply casting Int to any other type throws an exception:

println(0.asInstanceOf[String])

Throws a runtime exception:

Exception in thread "main" java.lang.ClassCastException: 
    java.lang.Integer cannot be cast to java.lang.String

But why it doesn't throw an exception in case with reducer?

Update 2:

Diving deeper,

def foo[A]: A = 1.asInstanceOf[A]

println(foo[String])                 // 1
println(foo[LocalDateTime])          // 1
println(foo[LocalDateTime].getClass) // java.lang.Integer

Source code

Andrii Abramov
  • 10,019
  • 9
  • 74
  • 96

2 Answers2

4

This is a partial answer.

You indeed cannot assign null to a value of type B that is not specified as B <: AnyRef without a cast. However, Scala generics are erased at runtime. The post-erasure post-boxing code would look something like that:

def reduceLeft(op: Function2): Object = {
  if (isEmpty)
    throw new UnsupportedOperationException("empty.reduceLeft")

  object reducer extends Function1 {
    var first = true
    var acc: Object = BoxesRunTime.boxToInteger(0)

    override def apply(x: Object): Unit =
      if (first) {
        acc = x
        first = false
      }
      else acc = op(acc, x)
  }
  self foreach reducer
  reducer.acc
}

Notice that the cast is gone too. There's zero information about what B was so there's no check to make.

Unlike there:

println(0.asInstanceOf[String])

This cast won't be erased, as String is a known type.

The erasure also explains why the foo calls work, as foo's erasure is basically:

def foo: Object = BoxesRunTime.boxToInteger(1)

Recall that println is defined as def println(any: Any): Unit, and post-scalac Any is replaced by Object. Therefore, when you're doing

println(foo[String])                 // 1
println(foo[LocalDateTime])          // 1

the resulting 1 is never assigned to anything that would do a runtime class check. The object you get out is passed directly to println.

But this will cause a ClassCastException since the result of foo needs to be cast down to String for printString call

def printString(s: String) = println(s)
printString(foo[String])

and this will also fail at runtime:

val str = foo[String]

because Scala will infer the type of str to be String, and then runtime will fail to cast to that type.


Now, the part of an answer I don't have is why they didn't do var acc: B = _ (which only works inside classes/objects) or var acc: B = null.asInstanceOf[B] (which works everywhere you can define a var). It could probably just be a line that survived through a bunch of refactors.

Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
2

In Scala 2.13 0.asInstanceOf[B] was changed to null.asInstanceOf[B] by Expression for all zero bits #8767

null.asInstanceOf[A] is more kosher than 0.

It doesn't matter here because the value is never used and is always re-assigned.

It's too bad someone started accusing var x: A = _ of undue ugliness. That remains the best expression of non-assignment.

In Scala 3 we could write

trait Foo[B]:                                                                                                                                        
  var acc: B = compiletime.uninitialized

which IMO conveys intention clearly.


Related What is happening with 0.asInstanceOf[B] in Scala reduceLeft implementation

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • Oh man, this has been already answered! I wasn't able to find it. Will close this as a duplicate. And thanks for pointing out to the PR. – Andrii Abramov Apr 08 '21 at 18:18