Many people argue that the type system in Scala is pretty complicated and strict. That’s true. And sometimes the complexity really confuses me in designing the type structure of a project. It really needs some thoughts before determining the type structure. Some compiling error or runtime error finally turns out to be the result of not understanding the type system of Scala enough. So in this post, I will talk about something about the type system in Scala, which I think is fundamental and important. I think the content will probably be incomplete and not well organized because of my restricted understanding of it..
Type System
The Any
is the base of all the types in Scala.
The AnyVal
is the subtype of Any
, and the base class of all the primitive value types such as Int
, Char
, Boolean
and so on.
The AnyRef
is the subtype of Any
, and the bse class of all the other objects in Scala. It is pretty like the java.lang.Object
in
Java. All the references are of the type AnyRef
.
While the Nothing
is the subtype of all the types in Scala, it extends everything in Scala.
I do think things like the inheritance, conform, etc are highly related with the concept of Variance(Covariance/Countervariance).
Class and Object
The concept of class
in Scala is pretty similar with that in other object oriented programming languages. And the class name denotes the type of the class.
The concept of object
in Scala is that it denotes the singleton object and the companion object
of the class with the same name.
In order to get the type of an object
, you need to use object.type
instead of the name of the object
. The companion object
has the same access rights as its related class.
Trait and Type Linearization
The trait
is something like the interface
in Java, but has differences. The trait
in Scala can have something like field
in
Java which is not allowed in Java for interface
. So trait
can contain both members and methods, just like normal class
does.
Unlike class
, the trait
can not have constructors, see blow for details.
Like that in Java, you can extends only one base class but with multiple traits in Scala. Thanks to the type linearization
, you
will not meet the diamond problem
in Scala. The diamond problem
is the problem that we are not sure what we want to refer to in
multiple inheritance, for example:
If type A defines a method commonMethod()
, and type B and type C both inherit type A and override the commonMethod()
, and then
type D extends both B and C, which version of commonMethod()
will super.commonMethod()
really call?
With type linearization
, there will be no such kind of ambiguity. It works as follows:
- start building a list of types, the first element is the type we’re linearing right now
- expand each supertype recursively and put all their types into this list
- remove duplicates from the resulting list, by scanning it from the left to right, removing all the types that already appeared
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trait A {def common = "A"}
// Any with AnyRef with A
trait B extends A { override def common = "B" }
// Any with AnyRef with A with B
trait C extends A { override def common = "C" }
// Any with AnyRef with A with C
class D extends B with C
new D common == "C"
// Any with AnyRef with A with B with Any with AnyRef with A with C with D
// Any with AnyRef with A with B with C with D
class E extends C with B
new E common == "B"
// Any with AnyRef with A with C with B with E
Simply thinking, the right wins providing the implementation and the left one decides the super call along the linearization.
Structural Subtyping, Refinement Types, Early Initialization
When a class inherits from another, the first class is said to be a nominal
subtype of the other one. It’s a nominal
subtype because each type has a name, and the names are explicitly declared to have a subtyping relationship. Scala also
supports structural subtyping
, where the subtyping relationship is of the types having the same members. To get structural
subtyping in Scala, use Scala’s refinement types
.
Following is an example:
1
2
3
4
5
6
7
8
9
10
11
12
13
using(new PrintWriter("data")){
writer => writer.println(new Date)
}
using(serverSocket.accept()){
socket => socket.getOutputStream().write("hello world".getBytes)
}
def using[T <: { def close():Unit }, S](obj: T)(operation: T => S) = {
val result = operation(obj)
obj.close()
result
}
In the above example, the upper bound of type T is a refined type
, which refines type Any
with a method close()
. It is a
structural type as well, as it has no name.
Another thing should be noticed is that structural subtyping
may cause negative impacts at runtime performance, since it
is implemented by using reflection.
Here is a problem I met before related with this topic.
Now, it’s time for early initialization.
Before that, I will first talk about one difference between class and trait in Scala. The class may take parameters in initialization, but the trait can not have parameters in initialization. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A (val a: Double, val b: Int){
require(b != 0)
val c = a / b
}
trait B {
val a: Double
val b: Int
require(b != 0)
val c = a / b
}
new A(2.0, 2) // works fine
new B{
val a = 2.0
val b = 2
} // this will throw runtime exception
Because trait can not take parameters in the process of initialization, new B{...}
is actually a refinement. It first
instantiates the trait B
, then refine it with concrete definitions of a and b, which is actually a new type which is
anonymous and is the subtype of trait B
. This is common, the super class should be instantiated first then be the subclass.
In order to make the refinements available in the instantiation of the trait, one way is to use the early initialization,
the other way is to use lazy val
s.
1
2
3
4
new {
val a = 2.0
val b = 2
} with B
The above codes illustrate the use of early initialization. Just first initialise a type with corresponding fields then mix in the trait. Although it also uses the refinement mechanism, they are different.
1
2
3
4
5
6
7
8
9
10
11
new B {
val a = 2.0
val b = 2
}
// Any with AnyRef with B with anno
new {
val a = 2.0
val b = 2
} with B
// Any with AnyRef with anno with B
Sure, the resulted type is different. (The anno denotes the anonymous type.)
And because pre-initialized fields are initialized before the superclass constructor is called, their initializers cannot
refer to the object that’s being constructed. Consequently, if such an initializer refers to this
, the reference goes to
the object containing the class or object that’s being constructed, not the constructed object itself. The pre-initialized
fields behave in this respect like class constructor arguments.
Path Dependent Type and Type Projection
The inner class types in Java can be represented as Type Projection
in Scala, while Scala provides a more strict thing
called path dependent type.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Parent{ // outer class
class Child // inner class
}
class Family(p: Parent){
type ChildOfParent = p.Child
def add(c: ChildOfParent) = ??? // only accept child of specific parent
}
class School{
def add(c: Parent#Child) = ??? // accept children of any parent
}
In the above example, it illustrates the difference between path dependent type and the type projection. The path dependent type is more strict restriction of type. I think this is very a nice mechanism for the type system. It’s very intuitive.
Coming soon
There are some other things remained to write in this post.
Actually I find another blog writing related things, see here.