- Map from Names To Members (Fields + Methods)
- Map from Names to Types
- How to Decide if an Object
o
Satisfies a TypeT
?
- A type
T
describes a set of values that behave in a certain way.
- A function type
S => T
is a contract between caller and callee:- Caller promises to give callee a value that behaves like
S
.- Callee promises to give caller a value that behaves like
T
.
- Compiler verifies that both sides obey the contract.
- As a result, certain run-time errors are guaranteed not to occur:
- Field or method not found
- Trying to call a non-function value
- ...
- (But others like divide-by-zero, null-dereference, etc. may)
Say we have a function of type T => U
val foo : T => U = x => ...
and an object of type S
.
val o : S = { ... }
- When is it safe to pass
o
tofoo
?
foo(o)
- When
S
is a subtype ofT
!
When can S
-typed values can be substituted for T
-typed values?
- an expression
e
has typeS
, and
S
is a subtype ofT
(writtenS
<:T
)
e
can be used in any context that requires aT
.
S
<: T
?ticker
Type From Last Time
type t = { def next(): Any
; def hasNext: Boolean }
s
<: t
?type s = { def hasNext: Boolean
; def next(): Any }
type t = { def next(): Any
; def hasNext: Boolean }
- Yes. Order of members does not matter.
- This is called "Permutation Subtyping" for structural types.
s
<: t
?type s = { def next(): Any
; def hasNext: Boolean }
type t = { def hasNext: Boolean
; def next(): Any;
; def foreach(f: Any => Unit): Unit }
- No!
s
does not guaranteeforeach
method, whicht
requires.
s
<: t
?type s = { def hasNext: String
; def next(): Any;
; def foreach(f: Any => Unit): Unit }
type t = { def next(): Any
; def hasNext: Boolean }
A: Yes B: No
s
<: t
?type s = { def hasNext: String
; def next(): Any;
; def foreach(f: Any => Unit): Unit }
type t = { def next(): Any
; def hasNext: Boolean }
s
provides an extra members whicht
doesn't need. That's okay...
s
hashasNext
, but it'sString
, notBoolean
.
- B: No
- Extra members are okay, but types for required ones must match up.
When structural type S
has more attributes than T
S
={ def f1: S1; ... ; def fn: Sn; def f0:S0 }
T
={ def f1: S1; ... ; def fn: Sn }
S
<:T
s
<: t
?type s = { def next(): Int;
; def hasNext: Boolean
; def foreach(f: Any => Unit): Unit }
type t = { def next(): Any
; def hasNext: Boolean }
A: Yes B: No
s
<: t
?type s = { def next(): Int;
; def hasNext: Boolean
; def foreach(f: Any => Unit): Unit }
type t = { def next(): Any
; def hasNext: Boolean }
next
method ins
returnsInt
rather thanAny
...
- But it's okay to return a more specific type!
- A: Yes
- Member types don't have to match exactly; they can be subtypes.
When S
fields are subtypes of T
fields
S
={ def f1: S1; ... ; def fn: Sn }
T
={ def f1: T1; ... ; def fn: Tn }
S1 <: T1
and ... andSn <: Tn
S <: T
So far, three subtyping rules:
Permutation : order of members doesn't matter
Width : extra members are okay
Depth : member types can be "more specific" than required
- These are common to many statically typed languages.
The subtyping (or "is-a") relation is reflexive
T
T
<:T
The subtyping (or "is-a") relation is transitive
S
<:T
T
<:U
S
<:U
How can we ask Scala if an expression e
has a certain type T
?
How can we ask Scala if an expression e
has a certain type T
?
scala> val blah: T = e
- First,
e
will be analyzed and (possibly) assigned some typeS
.
- Then, Scala will check if
S
<:T
.
- If yes, the assignment is well-typed. If not, error message.
scala> val blah: Int = 1
blah: Int = 1
scala> val blah: Boolean = 1
<console>:7: error: type mismatch;
found : Int(1)
required: Boolean
val blah: Boolean = 1
^
scala> val blah: Any = 1
blah: Any = 1
- Another Rule:
T
<:Any
for allT
.
Can make these "subtyping queries" more directly:
scala> (1:Int, 1:Any)
res20: (Int, Any) = (1, 1)
scala> 1:Boolean
... error ...
- Type ascription expression
e:T
declares an expected type fore
.
val
/var
/def
Declarations?type t1 = { val i : Int } ; type t3 = { def i : Int }
type t2 = { var i : Int } ; type t4 = { def i() : Int }
type t1 = { val i : Int } ; type t3 = { def i : Int }
type t2 = { var i : Int } ; type t4 = { def i() : Int }
object o1 { val i = 0 } ; object o3 { def i = 0 }
object o2 { var i = 0 } ; object o4 { def i() = 0 }
o1:t1
is well-typedo2:t2
is well-typedo3:t3
is well-typedo4:t4
is well-typed
- Let's make things more interesting...
type t1 = { val i : Int } ; type t3 = { def i : Int }
type t2 = { var i : Int } ; type t4 = { def i() : Int }
object o1 { val i = 0 } ; object o3 { def i = 0 }
object o2 { var i = 0 } ; object o4 { def i() = 0 }
scala> o1:t2 // Is this expression well-typed?
A: Yes B: No
var
means that the "context" (caller) may assign toi
...
- But
val
means immutable. So NO!
type t1 = { val i : Int } ; type t3 = { def i : Int }
type t2 = { var i : Int } ; type t4 = { def i() : Int }
object o1 { val i = 0 } ; object o3 { def i = 0 }
object o2 { var i = 0 } ; object o4 { def i() = 0 }
scala> o1:t3 // Is this expression well-typed?
A: Yes B: No
def
with no parameter list must be called with no parens.
- So can be implemented as a method or as a field.
- So YES!
type t1 = { val i : Int } ; type t3 = { def i : Int }
type t2 = { var i : Int } ; type t4 = { def i() : Int }
object o1 { val i = 0 } ; object o3 { def i = 0 }
object o2 { var i = 0 } ; object o4 { def i() = 0 }
scala> o1:t4 // Is this expression well-typed?
A: Yes B: No
def
with empty parameter list may be called with no parens.
- But must be implemented as a method.
- So NO!
type t1 = { val i : Int } ; type t3 = { def i : Int }
type t2 = { var i : Int } ; type t4 = { def i() : Int }
object o1 { val i = 0 } ; object o3 { def i = 0 }
object o2 { var i = 0 } ; object o4 { def i() = 0 }
scala> o2:t1 // Is this expression well-typed?
val
is required, so the "context" (caller) will only read...
- So it seems like a
var
should be okay.
- Scala says NO! Let's not worry why...
- Bottom line: Can't ever interchange immutable/mutable variables.
val
cannot be provided wherevar
requiredvar
cannot be provided whereval
required
val
can be provided wheredef
requiredvar
can be provided wheredef
required
val
cannot be provided wheredef(...)
required (not surprising)var
cannot be provided wheredef(...)
required (not surprising)
Two structurally identically classes:
class C1 { var i:Int = 0 } ; class C2 { var i:Int = 0 }
val x1 = new C1 ; val x2 = new C2
- Of course,
x1:C1
andx2:C2
.
- What about
x1:C2
?
Two structurally identically classes:
class C1 { val i:Int = 0 } ; class C2 { val i:Int = 0 }
val x1 = new C1 ; val x2 = new C2
Of course, x1:C1
and x2:C2
.
What about x1:C2
?
scala> x1:C2
<console>:11: error: type mismatch;
found : C1
required: C2
- What happened to structural subtyping ?!?
- Standalone objects are described with structural types.
- But class instances are described with nominal types.
new C1
has typeC1
When one class instance is an instance of another class
C
is a descendant ofD
in the class hierarchy
C
<:D
class C1 { val i:Int = 0 } ; class C2 { val i:Int = 0 }
- Instances of
C1
andC2
and have the same structure, but...
C1
<:C2
does not holdC2
<:C1
does not hold
- Isn't this strictly worse than structural typing?
- Well, yes and no...
Any ideas?
???
???
???
- Just consult the class hierarchy!
- This is declared directly in the source program.
- But: Can optimize structural subtyping to make it fast, too.
- Maybe the structural type of an object is not enough.
- Programmer might intend different behaviors even though structurally the same.
- Nominal typing requires programmer to declare intent.
- But: Structural systems can achieve this, too. (e.g. existential types)
C
, just refer to the name C
!abstract class IntList
{ val data: Int; val next: Option[IntList] }
- This is a big benefit compared to how recursive types work in structural systems (something called mu-types).
- Single-inheritance, nominal types comes with a cost...
- So, give up the dream of specifying only small, required bits of functionality (i.e. Structural Types)?
- And use declared, named types instead to get a couple other benefits (i.e. Nominal Types)?
- Of course not!
- Traits give us the best of both worlds!
- They are nominal, just like classes.
- Part of declared class/trait hierarchy.
- Because classes can mix in multiple traits...
- Okay to organize separate functionality into small, separate traits.
- So they can be organized into small structural pieces.
trait inti { val i : Int }
class D1 extends inti { val i = 0 }
class D2 extends inti { val i = 0 }
val y1 = new D1 ; val y2 = new D2
- Well-typed:
y1:D1
andy1:inti
andy2:D2
andy1:inti
- Ill-typed:
y1:D2
andy2:D1
class C1 { val i = 0 } ; class C2 { val i = 0 }
trait inti { val i : Int }
val z1 = new C1 with inti ; val z2 = new C2 with inti
// z1 : C1 with inti // z2 : C2 with inti
Note: the types of z1
and z2
mention with
.
- Well-typed:
z1:C1
andz1:inti
andz2:C2
andz1:inti
- Ill-typed:
z1:C2
andz2:C1
class C1 { val i:Int = 0 }
val x1 = new C1
scala> x1:{val i:Int}
res60: AnyRef{val i: Int} = C1@13c59de
scala> x1:{}
res61: AnyRef{} = C1@1b95cf5
- A nominal type can be a subtype of a structural type...
- Sometimes. The devil is in the details...
def applyFunc(f: Double => Double) = 3.14 + (f (3.14))
def addOne(i: Int) = 1 + i
val res = applyFunc(addOne)
A: Type Error (in applyFunc
)
B: Type Error (in call to applyFunc
)
C: res: Int = 7
D: res: Double = 7.28
E: Run-Time Exception
def applyFunc(f: Double => Double) = 3.14 + (f (3.14))
def addOne(i: Int) = 1 + i
val res = applyFunc(addOne)
- For call to succeed, type of
addOne
must be subtype off
.
- So, is
(Int => Int)
<:(Double => Double)
?
- Let's break this up into two steps...
When is (S => T1)
<: (S => T2)
?
- Callee (the "context") expects function output to be
T2
.
- So it's okay if caller provides an "actual" argument function...
- ... that produces return values of a more specific type
T1
.
- That is, if
T1
<:T2
.
When is (S1 => T)
<: (S2 => T)
?
- Callee (the "context") may call the function with args of type
S2
.
- So it's okay if caller provides an "actual" argument function...
- ... that can accept arguments of a more general type
S1
.
- That is, if
S2
<:S1
. Whoa, weird!
When is (In1 => Out1)
<: (In2 => Out2)
?
In2 <: In1
Out1 <: Out2
(In1 => Out1)
<:(In2 => Out2)
- Covariant in return type; Contravariant in argument type
Courtesy: Ranjit Jhala
Courtesy: Ranjit Jhala
Courtesy: Ranjit Jhala
Courtesy: Ranjit Jhala
def applyFunc(f: Double => Double) = 3.14 + (f (3.14))
def addOne(i: Int) = 1 + i
val res = applyFunc(addOne)
For call to succeed, type of addOne
must be subtype of f
.
So, is (Int => Int)
<: (Double => Double)
?
- Return type is okay (
Int
is a subtype ofDouble
).
- But argument type is not (
Double
is not a subtype ofInt
).
- B: Type Error (in call to
applyFunc
)
class Parent { def foo(p:Parent) = () }
class Child extends Parent
{ override def foo(p:Parent) = () }
- For
Child
class definition to be allowed,- The overridden
foo
method must be a subtype ofParent
's.
- Need
(Parent => Unit)
<:(Parent => Unit)
to be true.
- No problem there!
class Parent { def foo(p:Parent) = () }
class ProblemChild extends Parent
{ override def foo(c:ProblemChild) = () }
- For
ProblemChild
class definition to be allowed,- The overridden
foo
method must be a subtype ofParent
's.
- Need
(ProblemChild => Unit)
<:(Parent => Unit)
to be true.
- Uh oh, need (
Parent
<:ProblemChild
) by contravariance...
- So
ProblemChild
is not a well-typed subclass.
- Reflexivity and Transitivity
- Permutation, Width, and Depth
- Correspond to the Declared Class/Trait Hierarchy
- Contravariant Argument Types; Covariant Return Types