Scala Tutorial (2)

2008-10-07

Scala is a purely object-oriented language in the sense that all language constructs are objects. This includes all primitive type values, such as Char, Int, Long, Float etc. For example, it is perfectly legal in Scala to append a method call to a literal primitive value, as in 3.1415.round or 88.max(99). Scala defines the following basic types:

Type Format Range
Byte 8-bit Integer (-2^7 to 2 ^7-1)
Short 16-bit Integer (-2^15 to 2^15-1)
Int 32-bit Integer (-2^31 to 2^31-1)
Long 64-bit Integer (-2^64 to 2^64-1)
Char 16-bit Unicode character (0 to 2^16-1)
String A sequence of Chars
Float 32-bit IEEE 754 single-precision floating point number
Double 64-bit IEEE 754 double-precision floating point number
Boolean Logical value true or false

These data types are defined in the package scala, which is imported automatically along with scala.lang by the compiler. Extended functionality for these types (such as the cited max() function) is available via the scala.runtime API. Each type has an associated “rich wrapper class”, e.g for String there is a RichString class, for Int there is a RichInt class, and so on. Unlike Java, Scala does not distinguish between objects and primitive types. This implies that there are no boxing and unboxing functions and thus less to worry about the semantic details of these operations. Everything is simply an object. Behind the scenes the compiler performs optimisations that amount to auto-unboxing when arithmetic expressions are evaluated. Hence, the expression a + b c which is equivalent to method calls b.(c).+(a) is actually resolved to a primitive type arithmetic, just like in Java.

Scala is not only object-oriented in the sense that all variables are objects, irrespective of type, but that all functions including methods are also objects. Since functions and methods are objects, they are treated just like regular variables, which means that functions can be assigned to variables. They can also be used as parameters in function calls or as return values of functions and methods. A function value in the position of a function parameter creates a so-called higher-order function. In addition, Scala has a construct called a function literal which is basically a nameless function used in place of a function value, a bit like an anonymous method in Java, although the former are a lot more versatile. Furthermore, operators in Scala are methods. This makes some sort of operator overloading possible. Since +,-,*,/ etc. are just methods with funny names, you can define operators for your own data types. It is therefore possible to extend the language by adding APIs that contain class definitions for new data types along with their operators. This is illustrated by the following class definition for rational numbers. Rational numbers can be expressed as a fraction of two integer numbers. The following class defines the data type Rational as well as the four basic arithmetic operations for rational numbers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Rational(numerator: Int, denominator: Int) {

require(denominator != 0)

private val gcd = greatestCommonDivisor(numerator.abs,
denominator.abs)
val n = numerator / gcd
val d = denominator / gcd

def this(n: Int) = this(n, 1)

private def greatestCommonDivisor(a: Int, b: Int): Int =
if (b == 0) a else greatestCommonDivisor(b, a % b)

def + (that: Rational): Rational =
new Rational(n * that.d + d * that.n, d * that.d)

def - (that: Rational): Rational =
new Rational(n * that.d - d * that.n, d * that.d)

def * (that: Rational): Rational =
new Rational(n * that.n, d * that.d)

def / (that: Rational): Rational =
new Rational(n * that.d, d * that.n)

override def toString = n + "/" + d
}

The first line contains the head of the class with the so-called primary constructor, which is part of the class declaration in Scala. The expression in parentheses accepts two integer numbers that constitute the numerator and the denominator of the fraction. The next line that starts with require tests the denominator for zero and throws an exception if denominator = 0. As you can see, exceptions don’t need to be declared in the head of the class definition in Scala. The following three lines reduce the fraction by calculating the greatest common divisor and subsequently dividing the numerator and denominator by the result. These first four lines are not enclosed by a function definition block or any other block and thus constitute the body of the primary constructor. Next follows what’s called a auxiliary constructor in Scala. Auxiliary constructors are easy to spot, because they always start with this(). The expression def this(n: Int) = this(n, 1) provides a constructor that accepts a single integer number as an argument. This makes it easy to represent integer numbers as fractions where needed simply by setting the denominator implicitly to 1. For example, the expression new Rational(2,3) results in 2/3 and the expression new Rational(2) results in 2/1.

The method definition that follows the constructor code computes the greatest common integer divisor of two numbers using recursion. Since the recursive invocation stands in tail-call position in this instance, the Scala compiler optimises the generated byte code internally and creates a loop instead of a recursive function call. The four methods after the greatestCommonDivisor() method define the four basic arithmetic operations for fractions. In case of addition and subtraction, the numerators and denominators of both operands are multiplied with each other before the sum of the numerators is calculated. In doing so, Scala makes use of built-in operator precedence ( and / before + and -), which is determined lexically if the function or method name begins with a special operator characters. The expression n that.d + d that.n, is thus executed as n.(that.d).+.(d.*(that.n)). Multiplication and division are even easier to implement as the two fractions are simply multiplied by each other, or respectively multiplied by the reciprocal value. The results are then returned as new Rational objects. Since the constructor finds the GCD of the two integer numbers, the resulting fraction is automatically reduced. Finally, the last method toString outputs the fraction in a readable form. Overridden methods must be marked with the override keyword in Scala. Functions that don’t take arguments are written without parentheses, hence toString() becomes toString.

Class Hierarchy

Similar to Smalltalk, every class inherits from a single common superclass. The universal superclass is named Any in Scala. This is to say that any datum in Scala is of the type Any. The subclasses of Any fall into two categories: AnyVal (value) and AnyRef (object reference). The above listed basic data types, as well as the primitive types in Java, such as byte, int, float, etc., are direct descendants of AnyVal, while all other types are descendants of AnyRef. The Scala type AnyRef is therefore conceptually identical with java.lang.Object and it comes with analogous methods, e.g. equals(), hashCode, clone, wait, etc. Some of these are actually defined further up in the hierarchy, namely in Any. In addition, Scala defines the type scala.scalaObject which provides the common superclass for all objects in the Scala APIs. This works because Scala allows multiple inheritance via so-called traits. This will be explained later in more detail.

The nesting of this type hierarchy allows interoperability wit Java. For example:

1
2
3
var a: int = 2    // Java Integer
var b: Int = 3    // Scala Integer
println(a + b)    // 5

In addition, it is possible to use Java collections for storing Scala data types, or Scala collections for storing Java data types. Furthermore, Scala has several special data types. For example, scala.Null is defined as subtype of any class that derives from an object reference scala.AnyRef. It is analogous to the null literal in Java and can thus be assigned to object references. The type scala.Nothing is defined as a subtype of all types; it is employed in exceptions where an expression does not return a type. This happens situations where an exception is thrown in a place where a value is expected. Finally, there is the type scala.Unit that is used for functions that don’t return a value. This is Scala’s concession to imperative programming. It is analogous to the void keyword in Java.

PreviousNext