Pure OO Language - every value is an object. The type of each value is a class (assuming the language is based on classes).
abstract class Boolean {
def ifThenElse[T](t: => T, e: => T): T
def && (x: => Boolean): Boolean = ifThenElse(x, false)
def || (x: => Boolean): Boolean = ifThenElse(true, x)
def unary_!: Boolean = ifThenElse(false, true)
def == (x: Boolean): Boolean = ifThenElse(x, x.unary_!)
def == (x: Boolean): Boolean = ifThenElse(x.unary_!, x)
}
// partial implementation
class Int {
def + (that: Double): Double
def + (that: Float): Float
def + (that: Long): Long
def + (that: Int): Int
def & (that: Long): Long
def & (that: Int): Int
}
abstract class Nat {
def isZero: Boolean
def predecessor: Nat
def successor: Nat = new Succ(this)
def + (that: Nat): Nat
def - (that:Nat): Nat
object Zero extends Nat {
def isZero = true
def predecessor = throw new Error("0.predecessor")
def + (that: Nat) = that
def - (that:Nat) = if (that.isZero) this else throw new Error("negative number")
}
object Succ(n: Nat) extends Nat {
def isZero = false
def predecessor = n
def + (that: Nat) = new Succ (n + that)
def - (that:Nat) = if (that.isZero) return this else n - that.predecessor
}
The function type:
A => B
is an abbreviation for:
package scala
trait Function1[A, B] {
def apply(x: A): B
}
There are traits Function2, Function3, ... , Function22 for functions with 2 .. 22 parameters
A function such as:
(x: Int) => x * x
expands to:
{
class AnonFun extends Function1[Int, Int] {
def apply(x: Int) = x * x
}
new AnonFun
}
or shorter:
{
new Function1[Int, Int] {
def apply(x: Int) = x * x
}
}
The method:
def f(x: Int): Boolean = ...
is not itself a function value, but if used in a place where a Function type is expected, it will be converted to a function value. This is called an eta-expansion:
(x:Int) => f(x)
object List {
def apply[T]() = new Nil
def apply[T](x1: T): List[T] = new Cons(x1, new Nil)
def apply[T](x1: T, x2: T): List[T] = new Cons(x1, new Cons(x2, new Nil))
}
List()
List(1)
List(2,3)
There are two forms of polymorphism:
Consider a method assertAllPos which:
We can define the method signature as follows:
def assertAllPos(s: IntSet): IntSet
We can also define as follows:
assertAllPos(Empty) = Empty
assertAllPos(NonEmpty(...)) = either NonEmpty(...) or throws an exception
Notice that if the initial set is Empty, the method cannot return NonEmpty and vice-versa. We can then define the method signature more precisely as follows:
def assertAllPos[S <: IntSet](r: S): S
This:
<: IntSet
is an upper bound of the type parameter S, which means any subclass of type IntSet.
S <: T
means S is a subtype of T, while:
S >: T
means S is a supertype of T (or T is a subtype of S).
One can use lower bounds too, such as:
S >: IntSet
meaning S is a supertype of IntSet, meaning it can be IntSet, NonRef, AnyRef or Any. Sometimes both the upper bounds and the lower bounds can be specified, such as:
S >: NonEmpty <: IntSet
S is any type in the interval between NonEmpty and IntSet.
Assuming:
Ford <: Car
should the following be true?
List[Ford] <: List[Car]
Intuitively, it makes sense. A list of Fords is a specialised case of a List of Cars.
This kind of generic types is called covariant. Their subtyping relationship varies with the type parameter.
The co-variance brings forward the following issue in Java:
Ford [] fords = new Ford[] {new Ford("Focus")};
Car [] cars = fords;
car[0] = new Honda("Civic"); // ArrayStoreException thrown here at runtime
Ford f = a[0];
Java tags the array at creation with the type of objects that it can contain and will throw an ArrayStoreException at runtime, when trying to store an "incompatible" type into the array. This is a hole in the Java type system that exists for historical reasons. Originally, the language designers wanted to be able to write a method like:
sort(Object [] arr);
to sort any kind of array. That required co-variance.
Let q(x) be a property provable about objects x of type B. Then q(y) should be provable for objects y of type A where A <: B
In plain words:
If Ford <: Car then everything one can do with a Car instance, one should be able to do with a Ford instance.
List should be co-variant while Array should be not. Why? Because List is immutable while Array is mutable.
Say: C[T] is a parameterized type and A, B are types, such as: A <: B. There are three possible relationships between C[A] and C[B]:
C is covariant: C[A] <: C[B]
C is contravariant: C[A] >: C[B]
C is nonvariant: C[A] and C[B] are unrelated types (no subtypes)
In Scala, you can "program" the variance:
class C[+A] { ... } // covariant
class C[-A] { ... } // contravariant
class C[A] { ... } // nonvariant
Example:
type A = Car => Ford
type B = Ford => Car
The variance of the above function is:
A <: B
What can you do with type B? You can pass a Ford parameter to it and you will receive a Car in return. You can do the same with type A, You can pass a Ford parameter to it (which is a Car) and it will return a Ford (which is a Car). So type A satisfies the same contract as type B.
This can be generalised as follows:
If:
then:
(note that the inheritance direction of the arguments and return types is opposite). Functions are contra-variant in their argument type and co-variant in their return type.
trait Function1[-T, +U] {
def apply(x: T): U
}
The Scala compiler performs variance checks:
Case classes are similar to normal classes, except that they are preceded by the modifier: case:
trait Expr
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
For case classes , the Scala compiler generates automatically companion objects with factory methods:
object Number {
def apply(n: Int) = new Number(n)
}
object Sum {
def apply(e1: Expr, e2: Expr) = new Sum(e1, e2)
}
val x = Number(1)
Note: the last line, there's no new keyword when instantiating the object. We can then use pattern matching:
e match {
case pattern1 => expression
case pattern2 => expression
}
For instance:
def eval(e: Expr): Int = e match {
case Number(n) => n
case Sum(e1, e2) => eval(e1) + eval(e2)
}
Patterns are constructed from:
trait Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
}
val fruit = List("apples", "oranges", "bananas")
val nums = List(1, 2, 3, 4)
val diag3 = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))
val empty = List()
Type is inferred here, but you could also write:
val fruit: List[String] = List("apples", "oranges", "bananas")
val empty: List[Nothing]: List()
You can also use the cons operator :: to construct lists:
fruit = "apples" :: ("oranges" :: ("bananas" :: Nil))
nums = 1 :: (2 :: (3 :: (4 :: Nil)))
empty = Nil
Note: in Scala all operators that end in : associate to the right by convention, so we don't even need the parenthesis:
fruit = "apples" :: "oranges" :: "bananas" :: Nil
Operators ending in : are also seen as method calls of the right-hand operand, so we could write as well:
Nil.::(4).::(3).::(2).::(1)
From that perspective, :: is really a prepend operation.
All List operations can be expressed in terms of the following operations:
Lists can also be decomposed with pattern matching:
Nil - The Nil constant p :: ps - A pattern that matches a list with head p and a tail matching ps List(p1, ..., pn) - Same as p1 :: ... :: pn :: Nil
For example:
1 :: 2 :: xs - A List that starts with 1 and 2 x :: Nil - List length is 1 List(x) - Same as above List() - Same as Nil, empty list List(2 :: xs) - A List of Lists, the first of which starts with 2
Lists have methods for sorting, but assuming there is no such method, here is how a List could be sorted:
Given a List 7 :: 3 :: 9 :: 2 we could:
This sorting algorithm is called Insertion Sort.
def isort(xs: List[Int]): List[Int] = xs match {
case List() => List()
case y :: ys => insert(y, isort(ys))
}
def insert(x: Int, xs: List[Int]): List[Int] = xs match {
case List() => List(x)
case y :: ys => if (x < y) x :: xs else y :: insert(x, ys)
}