TheTypeSystem
Exisstential types: Are means of constructing types where we do not care
about the portion of the new type.
Let's see what we have in Java:
interface List<E> extends Collection<E> {
E get (int idx);
}
If we think about he backwords compatibility, we know that the List should work
also without generics.
List foo = ...
System.out.println(foo.get(0)).
In this case the returned value from the 'get' method is java.lang.Object.
But in order to return this type Java uxestential types where type parameter is
not specified. Thaht means that the only onformation known about the type parameter
that it must a subtype of java.lang.Object because all type parameters in Java are
subtypes or equivalent to java.lang.Object.
Is creating a list without type parameters equivalent to creating a List<Object>
The answer is no!
List foo = new ArrayList();
List<Object> bar = foo //unchecked conversion warning
Java doesn't consider the List and List<Object> the same type, but it will
automatically convert between the two sicne the practical difference between
these two types is minimal in Java.
In Scala the things a slightly different:
Suppose we have a Java class:
public class Test {
public static List makeList() {
return new ArrayList();
}
}
val l:java.util.List[_] = Test.makeList()
* Scala provides a convenience syntax for creating existential types using
the undersocre in place of a type parameter.
We can't add thjings to List[_] unless the compiler can determine that they're
all the same type.
def foo (x: List[_ >: Int]) = x //An existential type can have bounds.
Sometines the compiler restricts variance.
There's usually a simple transform that can fix this issue.
Example:
Let's suppose we would like to define an abstract List that supports the '++'
operator which allows it to be combined with other lists.
We would like to conver the a silt of Strings to list of Any, so we're going
to annotate the ItemType parameter as covariant.
trait List[+ItemType] {
def ++(other:List[ItemType]):List[ItemType]
}
error: covariant type ItemType occurs in
contravariant position in type List[ItemType] of value other
def ++(other : List[ItemType]): List[ItemType]
* Let's explain the problem:
If Cat <: Mammal and Dog <: Mammal
then
List[Cat] <: List[Mammal] and List[Dog] <: List[Mammal]
class ListDog extends List[Dog] {
def ++(other :List[Dog]): List[Dog] = {
...
}
}
class ListCat extends List[Cat] {
def ++(other :List[Cat]): List[Cat] = {
...
}
}
class ListMammal extends List[Mammal] {
def ++(other :List[Mammal]):List[Mammal] = {
...
}
}
val l:ListMammal = new ListCat //That should work since List[Cat] <: List[Mammal]
l ++ (List[Dog](new Dog)) //That should work since statically ++ method takes List[Mammal]
Now we've created a new List which should hold both cats and dogs while originally
it was List[Cat].
* The compiler is complaining that we are using the ItemType in contravariant
position (and as we saw previously it is true). But since we know that it should
be safe to take two lists of the same type and cast them up the ItemType hierarchy.
We are going to make the ++ method take a parameter. We can use this new type
parameter in the argument to avoid having ItemType in a contravariant position.
//Let's naively use another type parameter
trait List[+ItemType] {
def ++[OtherItemType](other:List[OtherItemType]):List[ItemType]
}
class EmptyList[ItemType] extends List[ItemType] {
def ++[OtherItemType](other:List[OtherItemType]) = other
}
error: type mismatch;
found: List[OtherItemType]
required: List[ItemType]
def ++[OtherItemType](other: List[OtherItemType]) = other
Since we did not impose any cinstraints on OtherItemType we got a type error.
Because ItemType is covariant we can make it as a lower bound to OtherItemType
trait List[+ItemType] {
def ++[OtherItemType >: ItemType]:List[OtherItemType]
}
class EmptyList[ItemType] extends List[ItemType] {
def ++[OtherItemType >: ItemType](other:List[OtherItemType]) = other
}
Because 'variance' is hard the safest thing to do when defining parametrized types
is defining everything as invariant andmark variance as needed.
A 'type parameter' is a typedefinition that's taken in as a parameter when
calling a method.
'Higher Kinded Types' are those that accept other types and construct a new type.
Just as parameters are key to constructing methods type parameters are the key
to constructing types.
Type parameter constraints:
def randomElement[A](x:Seq[A]):A //A is a type parameter, x is a parameter.
When calling a method:
randomElement[String](List("1","2","3"))
or the compiler can infer the type for us (if it can!):
randomElement(List("1","2","3")).
Higher-Kinded Types:
Those are types that use other types to construct new type.
type Callback[T] = Function[T,Unit]
This is a 'Higher Kinded type' called callback.
The type Callback isn't a complete type until it's parametrized.
Higher-Kinded types are also called 'type constructors'.
Higher kinded types are used to simplify signatures for complex types.
Variance:
Variance refers to the ability of type parameters to change or vary on higher
kinded types.
Variance is a way of decalring how type parameters can be changed to create
conformant types.
A higher kinded type T[A] is said to conform to T[B] if you can assign T[B] to
T[A] without causing any errors.
Invariance:
A higher kinded type that's invariant implies that for Any T,A and B if T[A]
conforms to T[B], then A must be equivalent type of B.
Invariance is the default for any higher kinded type parameter.
Covariance:
For any types T,A and B if T[A] conforms to T[B] then A <: B.
Example:
Cat <-- Mammal , T[Cat] <-- T[Mammal]
The Mammal and the Cat relationship is such that the Cat conforms to Mammal, if a
method requires something of type Mammal a value of Cat couldbe used.
If a type T were defined Covariant, then the type T[Cat] would conform to
T[Mammal], that is the method requiring a T[Mammal] would accept T[Cat].
An example:
List is defined as a covariant higher kinded type.
If we define a List[Any] and Any is a super type of all types, than we can use
List[String] or List[Integer] where List[Any] is wanted.
Creating covariant types is as easy as adding '+' before the type parameters.
Example:
class T[+A]{}
val x = new T[AnyRef]
val y:T[Any] = x //Perfectly leagal since AnyRef extends Any, then T[AnyRef]
// extends T[Any]
val z:T[String] = x
error: type mismatch;
found: T[AnyRef]
required: T[String]
val z : T[String] = x
The compiler tracks the usage of a higher kinded types, and insures that if it is
covariant, it occurs only in covariant positions.The same true for contravariance.
Contravariance:
The oposite of covariance.
For any type T,A and B if T[A] conforms to T[B], then A >: B
Example:
Cat --> Mammal, then T[Mammal] --> T[Cat]
If the type T is defined contravariant then a method expecting T[Cat]
would accept the value of T[Mammal].
Contravariance makes sence in the context of Function objects.
A Function object in contravariant on the argument type,
and covariant on the return type.
You should be able to cast a function of Any => String to String => Any,
but not vice versa.
Explanation:
trait Func[Arg,Return] {
def apply(x: Arg): Return
}
var func1 = new Func[Int,Any] = {
override def apply (x:Int) = x.toString
}
var func2 = new Func[Any,Int] {
override def apply (x:Int) = x.toInt
}
func1 = func2 //Compilation error
//We want to be able to use a function which receives an Int and everything
// less pecific than Int (Using at most Int)
//That being said, we would like our function to be able to return everything
// which is at least Any
//This is why the input param is contravariant and tghe output is covariant
//In other words: "Function promisses no less, and requires no more" ==>
//The most general as possible.
trait Func[-Arg,+Return] {
def apply(x: Arg): Return
}
Types
A 'type' is a set of information the compiler knows.
In Scala types are defined in two ways:
1. Defining a class, trait, or object.
2. Directly defining the 'type' using the type keyword.
Examples:
class ClassName
trait TraitName
object ObjectName
def foo(x:ClassName) = x --> Simple type
def bar(x:TraitName) = x --> Refers to a trait.
def baz(x.ObjectName.type) = x --> Refer's to the object's type
Using objects as parametes can greaatly help defining domain specific languages,
as yoo can embed words as objects that become parameters.
For example:
object Now
object simulate {
def once (behavior:() => Unit) = new {
def right (now:Now.type):Unit = ..
}
}
simulate once {() => someAction()} right Now
Type Path and Type Projection:
A path is a location where the compiler can find types.
A path could be cone of the following:
1. Empty path. When a type name could be found directly (there's am implicit empty)
path preceding it.
2. The path C.this whre C refers to a class. Using this directly in class C, is a
shorthand for the full path C.this.
3. The path p.x where p is the path and the x is the identifier of x.
A 'stable identifies' is an identifies the compiler knows for certain will
always be accessible from the path p.
A 'volatile type' is a type where the compiler can't be xertain that it's
members won't change. An exampel is an abstract class definition - the definition
changes as different subclasses extend it. An abstract class is a volatile type.
4. The path C.super or C.super[P] where C is a Class and refers to it's parent class.
Using super directly is shorthand for C.super. We are using it to destinguish
between identifiers defined on a class and a parent class.
In Scala we can reference a type with (.) or with (#).
The 'dot' refers to types found in specific object instance - those are path
dependent type.
"When a method is defined usign the dot operator to a particular type" +
"that type is bound to a psecific instance of an object"+"This means you can't" +
"use a type from a different instance of the same class to satisfy any type" +
"constraints made, using hte dot perator"
Example:
class A {
class B
def f (b:B) = println("Here is B!")
}
val a1 = new A
val a2 = new A
a2.f(new a1.B)
error: type mismatch;
found : a1.B
required: a2.B
a2.f(new a1.B)
^
Since there are no 'static classes' in Scala there's no A.B class.
The class type B is available only through instances of A.
In other words the compiler treats a.A and b.A as different classes.
Basically the full 'f' method definition is:
def f(b:this.B) = println("Here is B!") //Now it is more obvious that the
//parameter is bound to a specific instance
The (#) operatore is a looser restriction than the 'dot' operator.
It is called 'type projection' - which means refering to nested types without
requiring a path of object instances (referencing a nested type as if it weren't
nested).
class A {
class B
def f (b:A#B) = println("Here is B!")
}
val a1 = new A
val a2 = new A
a2.f(new a1.B) // "Here is B!"
The type keyword:
Scala allows types to be constructed using he tpe keyword.
We can create both abstract and concrete types.
Example:
type AbstractType
type ConcreteType = SomeFooType
type ConcreteType2 = SomeFooType with SomeBarType // This is a 'compound type'
Structural types:
In Scala a 'structural type' is created using hte type keyword and defining what
method signatures and variable signatures you expect on the desired type.
This allows a developer to define an abstract interface without requiring users
to extend some class or trait to meet this interface.
Handling resources with structural types:
object Resources {
type Resource = {
def close():Unit
}
def closeResource(r:Resource) = r.close()
}
Resources.close(System.in) //System.in is closed
We can see that when using structural types as parameters we can pass in any
type that fulfills the needed 'structure' - in our case everything that has
the def close():Unit method.
This is nece when dealing with libraries or classes we don't directly control
because we can not make them implement some behaviour via inheritance.
Structural typing also works within nested types and wqith nested types.
type T = {
type X = Int
def x:X //Nested type aliasw
type y//Nested abstract type
def y:Y
}
object Foo {
type X = Int //Concrete type
def x:X = 5
type y = String
def y:Y = "Hello, World!"
}
def test(t:T):t.X = t.x //Unstable type
"error: illegal dependent method type"
"def test(t : T) = t.x"
^
Scala doesn't allow a method to be defined such that the types used are
path-dependent on other arguments to the method.
In this case the return value of test would be dependent on the argument to
'test'.
If instead of using a 'path-dependent' type we wanted the type project against
X, we can modify our code.
def test(t:T):T#X = t.x
The compiler won't automatically infer this for us since the inference engine
tries to find the most specific type it can (in this case - t.X).
Type Constraints:
Those are rules associated with a type that must me met for a variable to match
the given type.
Type constraints take the form:
1. Lower bounds (Subtype restrictions)
2. Upper bounds (Supertype restrictions)
Lower bound - is where the type selected must be equal or supertype of the
lower bound restriction.
class A {
type B >: List[Int] //Define a lower bound restriction
def foo(a:B) = a
}
val x = new A {type B = Traversable[Int]} //Refine type A
x.foo(Set(1))
val y = new A {type B = Set[Int]}
error: overriding type B in class A with
bounds >: List[Int] <: Any
type B has incompatible type
val y = new A {type B = Set[Int]}
Just because the type restriction i=on type B requires it to be a super class of
List, doesn't mean that the arguments matching against type B needs to be from
List's hierarchy.
Here we enforced that the compile time constaint of type B must be s superclass
of List or List itself. But becaue Scala is a polymorphic language than an object
of class Set which subclasses Traversable can be used.
Upper bound - This restriction states that any type selected must be equal to
or lower, tha the upper bound type.Any selected class must be subclass from the
class or trait upper bound.
In the case of 'structural types' it means that what ever type is selected must
meet the structural type, but can have more information.
Example:
class A {
type B<: Traversable[Int]
def count(b:B) = b.foldLeft(0)(_+_)
}
val x = new A {type B = List[Int]} //Refine using lower type
x.count(List(1,2)) //Int = 3
x.count(Set(1,2))//Not assignable to refined type
error: type mismatch;
found: scala.collection.immutable.Set[Int]
required: x.B
x.count(Set(1,2))
^
val y = new A {type B = Set[Int]} //Works as type refinement
All types in Scala have an upper bound of Any and a lower bound of Nothing.
Since all types inherit from Any and are extended by Nothing.
* Avoid useless <: constraints.
Example: def sum[T <: List[Int]](t:T) = t.foldLeft(0)(_+_)
This 'sum' method has a redundant type constraint, since the type T doesn't
occur in the resulting value.
It could be as well written as follows:
def sum(t:List[Int]) = t.foldLest(0)(_+_)