Abimbola Esuruoso
Abimbola Esuruoso

Reputation: 4197

Nulls in Scala ...why is this possible?

I was coding in Scala and doing some quick refactoring in Intellij, when I stumbled upon the following piece of weirdness...

package misc

/**
 * Created by abimbola on 05/10/15.
 */
object WTF extends App {

  val name: String = name
  println(s"Value is: $name")
}

I then noticed that the compiler didn't complain, so I decided to attempt to run this and I got a very interesting output

Value is: null
Process finished with exit code 0

Can anyone tell me why this works?

EDIT:

  1. First problem, the value name is assigned a reference to itself even though it does not exist yet; why exactly does the Scala compiler not explode with errors???

  2. Why is the value of the assignment null?

Upvotes: 20

Views: 1517

Answers (3)

Silly Freak
Silly Freak

Reputation: 4231

I think @Andreas' answer already has the necessary info. I'll just try to provide additional explanation:

When you write val name: String = name at the class level, this does a few different things at the same time:

  • create the field name
  • create the getter name()
  • create code for the assignment name = name, which becomes part of the primary constructor

This is what's made explicit by Andreas' 1.1

package <empty> {
  class Example extends Object {
    private[this] val x: Int = _;
    <stable> <accessor> def x(): Int = Example.this.x;
    def <init>(): Example = {
      Example.super.<init>();
      Example.this.x = Example.this.x();
      ()
    }
  }
}

The syntax is not Scala, it is (as suggested by [[syntax trees at end of cleanup]]) a textual representation of what the compiler will later convert into bytecode. Some unfamiliar syntax aside, we can interpret this, like the JVM would:

  • the JVM creates an object. At this point, all fields have default values. val x: Int = _; is like int x; in Java, i.e. the JVM's default value is used, which is 0 for I (i.e. int in Java, or Int in Scala)
  • the constructor is called for the object
  • (the super constructor is called)
  • the constructor calls x()
  • x() returns x, which is 0
  • x is assigned 0
  • the constructor returns

as you can see, after the initial parsing step, there is nothing in the syntax tree that seems immediately wrong, even though the original source code looks wrong. I wouldn't say that this is the behavior I expect, so I would imagine one of three things:

  • Either, the Scala devs saw it as too intricate to recognize and forbid
  • or, it's a regression and simply wasn't found as a bug
  • or, it's a "feature" and there is legitimate need for this behavior

(ordering reflects my opinion of likeliness, in decreasing order)

Upvotes: 6

kiritsuku
kiritsuku

Reputation: 53358

why exactly does the Scala compiler not explode with errors?

Because this problem can't be solved in the general case. Do you know the halting problem? The halting problem says that it is not possible to write an algorithm that finds out if a program ever halts. Since the problem of finding out if a recursive definition would result in a null assignment can be reduced to the halting problem, it is also not possible to solve it.

Well, now it is quite easy to forbid recursive definitions at all, this is for example done for values that are no class values:

scala> def f = { val k: String = k+"abc" }
<console>:11: error: forward reference extends over definition of value k
       def f = { val k: String = k+"abc" }
                                 ^

For class values this feature is not forbidden for a few reasons:

  • Their scope is not limited
  • The JVM initializes them with a default value (which is null for reference types).
  • Recursive values are useful

Your use case is trivial, as is this:

scala> val k: String = k+"abc"
k: String = nullabc

But what about this:

scala> object X { val x: Int = Y.y+1 }; object Y { val y: Int = X.x+1 }
defined object X
defined object Y

scala> X.x
res2: Int = 2

scala> Y.y
res3: Int = 1

scala> object X { val x: Int = Y.y+1 }; object Y { val y: Int = X.x+1 }
defined object X
defined object Y

scala> Y.y
res4: Int = 2

scala> X.x
res5: Int = 1

Or this:

scala> val f: Stream[BigInt] = 1 #:: 1 #:: f.zip(f.tail).map { case (a,b) => a+b }
f: Stream[BigInt] = Stream(1, ?)

scala> f.take(10).toList
res7: List[BigInt] = List(1, 1, 2, 3, 5, 8, 13, 21, 34, 55)

As you can see it is quite easy to write programs where it is not obvious anymore to which value they will result. And since the halting problem is not solvable we can not let the compiler do the work for us in non trivial cases.

This also means that trivial cases, as the one shown in your question, could be hardcoded in the compiler. But since there can't exist a algorithm that can detect all possible trivial cases, all cases that are ever found need to be hardcoded in the compiler (not to mention that a definition of a trivial case does not exist). Therefore it wouldn't be wise to even start hardcoding some of these cases. It would ultimately result in a slower compiler and a compiler that is more difficult to maintain.

One could argue that for an use case that burns every second user it would be wise to at least hardcode such an extreme scenario. On the other hand, some people just need to be burned in order to learn something new. ;)

Upvotes: 12

Andreas Neumann
Andreas Neumann

Reputation: 10894

1.) Why does the compiler not explode

Here is a reduced example. This compiles because through given type a default value can be inferred:

class Example { val x: Int = x }

scalac Example.scala 
Example.scala:1: warning: value x in class Example does nothing other than call itself recursively
class Example { val x: Int = x }

This does not compile because no default value can be inferred:

class ExampleDoesNotCompile { def x = x }

scalac ExampleDoesNotCompile.scala 
ExampleDoesNotCompile.scala:1: error: recursive method x needs result type
class ExampleDoesNotCompile { def x = x }

1.1 What happens here

My interpretation. So beware: The uniform access principle kicks in. The assignment to the val x calls the accessor x() which returns the unitialized value of x. So x is set to the default value.

class Example { val x: Int = x }
                             ^
[[syntax trees at end of                   cleanup]] // Example.scala
package <empty> {
  class Example extends Object {
    private[this] val x: Int = _;
    <stable> <accessor> def x(): Int = Example.this.x;
    def <init>(): Example = {
      Example.super.<init>();
      Example.this.x = Example.this.x();
      ()
    }
  }
}                            ^

2.) Why the value is null

The default values are determined by the environment Scala is compiled to.

In the example you have given it looks like you run on the JVM. The default value for Object here is null.

So when you do not provide a value the default value is used as a fallback.

Default values JVM:

byte  0
short 0
int   0
long  0L
float 0.0f
double    0.0d
char  '\u0000'
boolean   false
Object    null // String are objects.

Also the default value is a valid value for given type: Here is an example in the REPL:

scala> val x : Int = 0
x: Int = 0

scala> val x : Int = null
<console>:10: error: an expression of type Null is ineligible for implicit conversion
val x : Int = null
                   ^
scala> val x : String = null
x: String = null

Upvotes: 14

Related Questions