Strange behavior of Scala compiler when initializing a class with a lazy argument

123 views Asked by At

How possible that the first is correct Scala code but the second won't even compile?

The one that does compile

object First {
  class ABC(body: => Unit) {
    val a = 1
    val b = 2
    println(body)
  }

  def main(args: Array[String]): Unit = {
    val x = new ABC {
      a + b
    }
  }
}

This one doesn't compile on Scala 2.11 and 2.12

object Second {
  class ABC(body: => Int) {
    val a = 1
    val b = 2
    println(body)
  }

  def main(args: Array[String]): Unit = {
    val x = new ABC {
      a + b
    }
  }
}
2

There are 2 answers

11
pedromss On BEST ANSWER

It's not strange at all. Let's look at the first example:

You declare your class ABC to receive a pass by name parameter that returns Unit and you think this snippet:

   val x = new ABC {
      a + b
    }

is passing that body parameter, it isn't.What's really happening is:

val x = new ABC(()) { a + b }

If you run that code you will see that println(body) prints () because you're not passing a value for your body parameter, the compiler allows it to compile because as the scaladoc states there is only 1 value of type Unit:

Unit is a subtype of scala.AnyVal. There is only one value of type Unit, (), and it is not represented by any object in the underlying runtime system. A method with return type Unit is analogous to a Java method which is declared void.

Since there is only one value the compiler allows you to omit it and it will fill in the gap. This doesn't happen with singleton objects because they don't extend AnyVal. Just has the default value for Int is 0 the default value for Unit is () and because there is only this value available the compiler accepts it.

From documentation:

If ee has some value type and the expected type is Unit, ee is converted to the expected type by embedding it in the term { ee; () }.

Singleton objects don't extend AnyVal so they don't get treated the same.

When you use syntax like:

new ABC {
 // Here comes code that gets executed after the constructor code. 
 // Code here can returns Unit by default because a constructor always 
 // returns the type it is constructing. 
}

You're merely adding things to the constructor body, you are not passing parameters.

The second example doesn't compile because the compiler cannot infer a default value for body: => Int thus you have to explicitly pass it.

Conclusion

Code inside brackets to a constructor is not the same as passing a parameter. It might look the same in same cases, but that's due to "magic".

0
Suma On

You cannot pass a single argument to a constructor in curly braces, because this would be parsed as defining an anonymous class. If you want to do this, you need to enclose the curly braces in normal braces as well, like this:

new ABC({
  a + b
})

As for why does compiler accept new ABC {a + b}, the explanation is a bit intricate and unexpected:

  1. new ABC {...} is equivalent to new ABC() {...}
  2. new ABC() can be parsed as new ABC(()) because of automatic tupling, which is a feature of the parser not mentioned in the specs, see SI-3583 Spec doesn't mention automatic tupling. The same feature casues the following code to compile without an error:

    def f(a: Unit) = {}
    f()
    
    def g(a: (Int, Int)) = {}
    g(0,1)
    

    Note the call produces a warning (even your original example does):

    Adaptation of argument list by inserting () has been deprecated: this is unlikely to be what you want.

    The warning is produced since 2.11, see issue SI-8035 Deprecate automatic () insertion.