Tuesday, July 10, 2012

Choices with def and val in Scala

In Scala, one can define class properties with either "def" or "val." Using "def", one can define the property with or without parentheses, and with or without the equal sign. Why is this? What are the consequences?

For illustrative purposes, we'll use a block of code that printing to the console, then returns an int. This shows when the code runs.

carrot.scala:
import System.out._
class Carrot {
   val helloTen = { println "hello"; 10 }
}
When a val is initialized to a block of code, the code runs at construction, and the property contains 10. The block only runs once. You can see this in the REPL:

scala> :load carrot.scala
...
scala> val c = new Carrot()
hello
c: Carrot = Carrot@5fe2b9d1
scala> c.helloTen
res5: Int = 10

If you change val to def, that block of code instead becomes a method. It prints and returns 10 every time it is accessed.

carrot.scala:
import System.out._
class Carrot {
   def helloTen = { println "hello"; 10 }
}

Use the REPL to observe that the code runs at every access:

scala> :load carrot.scala
...
scala> val c = new Carrot()
c: Carrot = Carrot@5689a400
scala> c.helloTen
hello
res7: Int = 10
scala> c.helloTen
hello
res8: Int = 10

Now I'm going to stop showing you what the REPL prints out, because that's boring to read. Try it for yourself.

So "def" or "val" determines whether the block of code runs only at construction, or at every access. There's another consequence: in a subclass, you can override a def with a val or a def, but you can only override a val with a val. When Scala has a val, it knows the value of that expression will never change. This is not true with def; therefore declaring something as val says more than declaring a def.

Let's move on with "def" because it's more flexible for extension. Next decision: use parentheses or not? We can choose between
def helloTen = { println "hello"; 10 }
and
def helloTen() = { println "hello"; 10 }

The consequences of this decision are: if you include the parentheses in the definition, then the property can be accessed with or without parentheses. If you do not include parentheses in the definition, then the property must be accessed without parentheses.

Here's a summary of our options:
With () No ()
val n/a runs at construction;
override with val;
access with no ()
def runs at every access;
override with val or def;
access with or without ()
runs at every access;
override with val or def;
access with no ()

The idiomatic rule of thumb is: use parentheses if the method changes state; otherwise don't.

Now here's a subtlety. If we use def with or without parentheses, the property can be overridden in a subclass by a def with or without parentheses (or a val without parentheses). This has strange consequences: If I subclass Carrot and override the property, but change whether parentheses follow the property declaration, then the interface of the subclass does not match the superclass.

carrot.scala:
import System.out._
class Carrot {
    def helloTen = { println ("hello"); 10 }
}

class Soybean extends Carrot {
    override def helloTen() = 14
}

On a Carrot, I can access helloTen only without parentheses. On a Soybean, I can access the property with or without parentheses. If I cast a Soybean to a Carrot, then I can access helloTen only without parentheses. Either way, the Soybean's helloTen property evaluates to 14, as a good polymorphic method should.

Stranger still, reverse it: if Carrot defines helloTen with parentheses and Soybean without, then a Carrot (or a Soybean cast to a Carrot) will helloTen with or without parentheses -- but a Soybean will only helloTen without parentheses! Therefore, a method call that works on the superclass errors on the subclass. Does this sound like a violation of LSP to you? Technically instances of the subclass can be substituted for instances of the superclass, but the interface of the subclass is smaller than that of the superclass. Wah? If this makes sense to you, please comment.

For another method-declaration subtlety, consider the equals sign.

I'm running Scala 2.9.2.

1 comment:

  1. Very thought provoking post. It does seem like there is a violation of LSP? I am curious.

    ReplyDelete