Contents
- Optional Class Parameters
- Optional Trait Parameters
- Early Definition
- Required Trait Parameters
- Abstract Class Parameters
- Type Parameters
- Caveats
Optional Class Parameters
In Java, a typical idiom for initializing an object that has a large number of optional parameters, of which only a few usually get set, is to construct the object and then call setter functions to customize each of the optional parameters. While this technique can be convenient, it leaves open the possibility that the setter might get called later on in the objects lifecycle at a time when changing that value could cause problems.One solution to this problem is to use the builder pattern. This solution is available in Scala as well, and can be taken a step farther than in Java by using the type-safe builder pattern.
The type-safe builder can be overly complicated for many situations. Sometimes it would be nice to have something simpler than even the simplest of builders.
Scala 2.8 will have named parameters with default values, which will make it pretty easy to create classes that have optional parameters, although you might not want to do this if you have 30 optional parameters. Meanwhile, there is another approach you can use: overriding
val
s.
The approach is pretty simple: you define a base class with a constructor that includes all of the required parameters, and you then add a
val
for each
of the optional parameters.
When you want to create an instance of that class that sets some
of the optional parameters, you create an anonymous subclass by adding
a set of braces after the new
statement
that creates the instance, and inside
the braces you override each val
that you want to set.
In this example we define a
Car
class that represents
a few pieces of information about a car.
model
and color
are required parameters
and appear in our constructor.
Our optional parameters are hasRadio
and hasSunRoof
,
so we make those
val
s rather than constructor parameters, and we assign them their
default values.
We include a toString
method so we can easily see
the results.
class Car(model:String, color:String) { val hasRadio = false val hasSunRoof = false override def toString() = { "Car{"+ "model="+model+ ",color="+color+ (if (hasRadio) ",hasRadio" else "")+ (if (hasSunRoof) ",hasSunRoof" else "")+ "}" } }The normal use would be to call the constructor with no additional arguments:
val c1 = new Car("Ford", "red") println(c1) //Car{model=Ford,color=red}To specify one of our optional arguments, we add a code block to the
new
call, which creates an anonymous subclass in which
our val
overrides the default:
val c2 = new Car("Chevy", "blue") { override val hasRadio = true } println(c2) //Car{model=Chevy,color=blue,hasRadio}We can pass in values from the caller's context rather than constants:
val myHasSunRoof = true val c3 = new Car("Honda", "white") { override val hasSunRoof = myHasSunRoof } println(c3) //Car{model=Honda,color=white,hasSunRoof}
Optional Trait Parameters
You can use this same approach to pass in values for instance variables in traits, which don't have constructor parameters. For example, say we define a trait for an optionalTouring
package for our car:
trait Touring { val hasNavSystem = false val hasExtraSuspension = false val hasTowHitch = false val hasRunningBoards = false override def toString() = { super.toString()+ "+Touring{"+ (if (hasNavSystem) "navSystem," else "") + (if (hasExtraSuspension) "extraSuspension," else "") + (if (hasTowHitch) "towHitch," else "") + (if (hasRunningBoards) "runningBoards," else "") + "}" } }Now we can create an instance of a
Car with Touring
and pass in values for some of those "optional constructor parameters"
defined in the Touring
trait:
val c4 = new Car("Honda","white") with Touring { override val hasSunRoof = true //from Car override val hasNavSystem = true //from Touring override val hasRunningBoards = true //from Touring } println(c4) //Car{model=Honda,color=white,hasSunRoof}+Touring(hasNavSystem,hasRunningBoards,}NOTE: Due to a bug in older versions of Scala, at least through 2.7.6, overriding a
val
on a trait as in the above example does not work.
This does work properly in Scala 2.8.0 (at least it does in the
20091006 nightly build).
Early Definition
You may have a situation in which some of theval
s that you are
initializing in a trait or class depend on other val
s.
In this case, overriding a val
as we did above may not give you
the result you want:
the initializer of the superclass runs to completion before the
initializer of the subclass,
which means all of the vals in the superclass get set before any
of the overriding vals are evaluated.
For example, say we modify our Touring trait by adding a
maxTowWeight
value, as shown in bold below:
trait Touring { val hasNavSystem = false val hasExtraSuspension = false val hasTowHitch = false val hasRunningBoards = false val maxTowWeight = if (!hasTowHitch) 0 else { if (hasExtraSuspension) 1500 else 1000 } override def toString() = { super.toString()+ "+Touring{"+ (if (hasNavSystem) "navSystem," else "") + (if (hasExtraSuspension) "extraSuspension," else "") + (if (hasTowHitch) "towHitch," else "") + (if (hasRunningBoards) "runningBoards," else "") + "maxTowWeight="+maxTowWeight + "}" } }When we instantiate a
Car with Touring
the constructor
code for Touring
executes before the constructor code
for the new class.
In particular, val maxTowWeight
gets evaluated before
the overriding values are evaluated, so it always ends up with a
value of zero:
val c5 = new Car("Honda","white") with Touring { override val hasTowHitch = true } println(c5) //Car with Touring = Car{model=Honda,color=white,hasRadio=false,hasSunRoof=false}+Touring{towHitch,maxTowWeight=0}Scala provides a mechanism to address this issue: Early Definition (Scala Language Specification, section 5.1.6). The
val
s
that you specify in the Early Definition block are evaluated
in the context of the calling class,
then that set of values is placed into the context of the new class
being instantiated such that all of those values are available at
the beginning of the process of instantiation,
even before the initializer for Object
is executed.
In this way, any expression which uses one of those vals will have
access to the value provided in the Early Definition.
It could be used with our
Car
example like this:
val c6 = new { override val hasTowHitch = true } with Car("Honda","white") with Touring println(c6) //Car with Touring = Car{model=Honda,color=white,hasRadio=false,hasSunRoof=false}+Touring{towHitch,maxTowWeight=1000}A class definition for the above example could look like this:
class TouringCarWithHitch(name:String, color:String) extends { override val hasTowHitch = true } with Car(name,color) with Touring { //normal class overrides and additional elements here } val c7 = new TouringCarWithHitch("Honda","white") //c7 is the same as c6 (but we have not implemented ==)
Required Trait Parameters
If you want to define a trait that has required parameters rather than optional parameters, you can omit the value from the declarations and instead specify only the type, which causes theval
to be abstract.
For example, if we want to make the hasTowHitch
and hasNavSystem
parameters to our modified Touring trait be required,
that would look like this:
trait Touring { val hasNavSystem:Boolean //abstract (no value) val hasExtraSuspension = false val hasTowHitch:Boolean //abstract (no value) val hasRunningBoards = false val maxTowWeight = if (!hasTowHitch) 0 else { if (hasExtraSuspension) 1500 else 1000 } override def toString() = { super.toString()+ "+Touring{"+ (if (hasNavSystem) "navSystem," else "") + (if (hasExtraSuspension) "extraSuspension," else "") + (if (hasTowHitch) "towHitch," else "") + (if (hasRunningBoards) "runningBoards," else "") + "maxTowWeight="+maxTowWeight + "}" } }Now when we declare a concrete instance of this class, we are required to define values for those two variables else we will get a compiler error. Since the base declaration is now abstract, we omit the
override
keyword on those val
s:
val c8 = new Car("Honda","white") with Touring { override val hasSunRoof = true //from Car val hasNavSystem = true //from Touring; required override val hasRunningBoards = true //from Touring; optional val hasTowHitch = false //from Touring; required } println(c8) //Car{model=Honda,color=white,hasSunRoof}+Touring(hasNavSystem,hasRunningBoards,maxTowWeight=0}
Abstract Class Parameters
Sometimes it is convenient to use an abstractval
rather than a
constructor parameter for abstract classes.
For example, say you have a Service and you want to define a set of
case classes for service messages.
The base class should have a reference to the Service object so that it
can easily be processed by generic service methods,
but each case class should also have the same reference as a case
value for easy matching.
For consistency, since these are the same value, the name should be the same.
You could do this by defining the base class with one parameter
declared as a val
to make it accessible, then
define the case classes to override that value,
like this:
abstract class Service abstract class ServiceMessage(val service:Service) case class ServiceStart(override service:Service) extends ServiceMessage(service) case class ServiceStop(override service:Service) extends ServiceMessage(service)The case class automatically adds a
val
keyword to each
of our parameters,
so we need to specify the override
keyword,
but can omit the val
keyword.
We can simplify our case classes a bit by changing the base class
val
from a constructor parameter to an abstract
val
, like this:
abstract class Service abstract class ServiceMessage { val service:Service } case class ServiceStart(service:Service) extends ServiceMessage case class ServiceStop(service:Service) extends ServiceMessageNot only have we dropped the
override
keyword,
but we are also not passing the service
parameter to
the superclass.
The implied val
keyword on the case class parameters
creates a concrete instance of the service
parameter
that overrides the abstract value defined in the base class.
Type Parameters
Just as scala has value parameters, concrete value members and abstract value members, it likewise has type parameters, concrete type members and abstract type members. The approach used above on values can generally by applied to types as well: rather than defining a class with a type parameter, you can often define that class with a type member. If the type is a required type that must be overridden by the extending class, make the type member abstract; if you want the subclass to be able to default to the type used in the superclass, use a concrete type and let the subclass use theoverride
keyword if it wants to override that type.
Bill Venners has a nice blog post where he discusses the question of when to use a type parameter and when to use an abstract type member, with a reference to an interview with Martin Odersky where he talks about abstract type members in comparison to instance variables.
Caveats
Although in many ways you are free to choose between using a constructor parameter versus a class member, they are not entirely equivalent. In particular, once you start building up class hierarchies using abstract and concrete members with overrides, you have to be careful that the initialization order is what you expect. In the Early Definition section above I gave one example of how values can fail to initialize correctly due to ordering issues. That one is pretty easy to understand, but they can sometimes be far more subtle and hard to spot.One thing you can do that will sometimes fix such problems is to use the
lazy
keyword on your value members in order to
get lazy initialization.
This causes initialization of the value to be delayed until
the first time it is used,
rather than being eagerly initialized when the class is initialized.
Note that if you declare a concrete variable as lazy
,
then an overriding
instance of that variable must also be declared as lazy
;
if the original concrete variable is not lazy
,
the overriding variable
can not be lazy
.
Note that overriding a
val
in Scala is not the same as
declaring a variable of the same name in a subclass in Java.
Consider this Java test program Test.java
:
public class Test { public static void main(String[] args) { (new Test1()).test1(); (new Test2()).test1(); (new Test2()).test2(); } } class Test1 { public int t = 1; public void test1() { System.out.println("t="+t); } public void test2() { System.out.println("t="+t); } } class Test2 extends Test1 { public int t = 2; public void test2() { System.out.println("t="+t); } }and the apparently equivalent Scala test program
Test.scala
(where I have used
Java-like syntax where possible so that you can run "diff" on the
two files):
object Test { def main(args: Array[String]) { (new Test1()).test1(); (new Test2()).test1(); (new Test2()).test2(); } } class Test1 { val t = 1 def test1() { System.out.println("t="+t); } def test2() { System.out.println("t="+t); } } class Test2 extends Test1 { override val t = 2 override def test2() { System.out.println("t="+t); } }Copy these out to
Test.java
and Test.scala
,
then compile and run each one
(don't try to compile both and then run both in the same
directory, as the class files will collide).
The Java test prints this out:
t=1 t=1 t=2The Scala test prints this out:
t=1 t=2 t=2Note the difference in the middle line, where we have called
Test2.test1()
.
The Java program prints 1, but the Scala program prints 2.
This is because the declaration of t
in Test2
in Java does not
override the value in Test1
, it
shadows it.
The Test1
value of t
is still
there, and it used by any method in Test1
that refers to that variable.
In Scala, by contrast, references to
t
in Test1
refer to
the overridden value provided by Test2
.
Scala can do this because, consistent with the
Uniform Access Principle,
a variable in Scala is accessed by a pair of functions to
get and set its value.
When a value is overridden, that creates new access functions
in the subclass that override the access functions in the base class.