Com S 541 Lecture -*- Outline -*- * Inheritance definition by extension (defining differences) with dynamic binding ------------------------------------------ A (SUPER)CLASS: STACK package misc; import scala.collection.mutable.ArrayBuffer; /** Stacks */ class Stack[T]() { /** The contents of the stack. */ private val elems = new ArrayBuffer[T](); /** Push v onto the top of this stack. */ def push(v: T): Unit = { elems+v } /** Pop the top of this stack off. */ def pop(): Unit = { elems.trimEnd(1) } /** Return the top of this stack. */ def top(): T = elems(elems.length); } ------------------------------------------ ------------------------------------------ A SUBCLASS, STACKWITHLENGTH package misc; /** Stacks */ class StackWithLength[T]() extends Stack[T]() { /** The length the stack. */ private var size = 0; /** The length of this stack. */ def length = size; } ------------------------------------------ Q: How to define push, pop, and top? override def push(v: T): Unit = { super.push(v); size = size + 1; } override def pop(): Unit = { super.pop(); size = size - 1; } ** Inheriting data Show the layout of storage in instances of StackWithLength the Stack is *not* contained in the StackWithLength (that would be a different design) ** Inheriting operations Explain meaning of this and super -infinite loops from calling with this Q: Where does code execution start from? some method -- always! ------------------------------------------ STEPS IN MESSAGE SENDING let receiver = object that is sent message let static-class = class where code being executed is defined let rc = if receiver is not "super" then receiver's (dynamic) class else if receiver is "super" then superclass of static-class 1. [fetch] look for method in class rc (a) if found: (b) if not found and rc is not "Any": (c) if not found and rc is "Any": 2. [execute] bind this to bind formals to execute the code of the method ------------------------------------------ ... 1(a) go to step 2 ... 1(b) set rc to superclass of rc, goto 1 ... 1(c) error (message not understood) ... receiver, actuals *** dispatch examples without super ------------------------------------------ MESSAGE PASSING EXAMPLE package misc; object DynamicDispatch { class C { def m1() = m2(); def m2() = "C"; } class D extends C { override def m2() = "D"; } def main(args: Array[String]) = { Console.println("new C().m1() = " + new C().m1()); Console.println("new D().m1() = " + new D().m1()); var r = new java.util.Random(); val x: C = if (r.nextBoolean()) new C() else new D(); Console.println("x.m1() = " + x.m1()); } } ------------------------------------------ Q: what is the result of new D().m1() ? returns "D". Discuss how, draw picture Q: What if we added another method to D... def m3 = super.m2(); and then ran new D().m3 ? Q: What if m2's body was this.m2() instead? *** dispatch examples with super ------------------------------------------ MESSAGE PASSING EXAMPLE WITH SUPER package misc; object DynamicDispatchSuper { class C { def m1() = m2(); def m2() = "C"; def m3() = "m3"; } class D extends C { override def m2() = super.m3(); override def m3() = "E"; } def main(args: Array[String]) = { Console.println("new C().m1() = " + new C().m1()); Console.println("new D().m1() = " + new D().m1()); } } ------------------------------------------ Q: What is the result (if any) of the expression "new C().m1()"? It's the string "C". Q: What is the result (if any) of the expression "new D().m1()"? It's the string "m3". If you got "E" you don't see how super works. *** Example: CachedStack ------------------------------------------ CACHED STACK package misc; /** Stacks with a Cache */ class StackWithCache[T <: AnyRef]() extends Stack[T] { /** The cache. */ private var cache: T = null; /** Push v onto the top of this stack. */ override def push(v: T): Unit = { if (cache != null) { super.push(cache); } cache = v; } } ------------------------------------------ Q: How would you define pop and top? /** Pop the top of this stack off. */ override def pop(): Unit = { if (cache == null) { super.pop(); } else { cache = null; } } /** Return the top of this stack. */ override def top(): T = { if (cache != null) { cache } else { super.top(); } } *** How to define toString(), refactoring Q: How to define toString() for StackWithCache? Note can't access elems (but could make that protected). Simply calling super.toString() won't work, as want the cache to be inside the Stack(...) listing, not after the ")". ------------------------------------------ INHERITANCE BY FACTORING OUT COMMON PARTS in class Stack[T]: class Stack[T]() { /* ... */ override def toString():String = { "Stack(" + elementsString + ")" } def elementsString = { val buf = new StringBuffer(); var first = true; for (val e: T <- elems.elements) { if (!first) { buf.append(", "); first = false; } buf.append(e.toString()); } buf.toString(); } } FOR YOU TO DO Define whatever methods are *needed* to have toString work for StackWithCache ------------------------------------------ // in StackWithCache override def elementsString = { super.elementsString + (if (cache != null) cache else "") } conclusion: subclassing allows code sharing abstraction ** traits and multiple inheritance *** Design issue: Single vs. multiple inheritance ------------------------------------------ SINGLE VS. MULTIPLE INHERITANCE def: a language has *single inheritance* if each class has def: a language has *multiple inheritance* if each class may have Motivating problem: What if want StackWithCacheLength? ------------------------------------------ ... at most one superclass examples: Smalltalk, Java note, inheritance is thus hierarchical (tree like) (In Smalltalk, class hiererchy is exactly parallel Stack class inherits from Object class) ... more than one superclass examples: CLOS, C++, ... (often such languages also permit a class to have no superclasses) with single inheritance StackWithCacheLength has to be subclass of either StackWithLength or StackWithCache problems: what if two (or more) define same instance variable or same method? efficiency (have to hash to get instance vars) initialization make a chart of advantages and disadvantages *** traits something like Java interfaces... Basic idea from Scharli, Ducasse, Nierstrasz, and Black's paper: "Traits: Composable Units of Behavior" in Proc ECOOP 2003. ------------------------------------------ TRAITS Goal: multiple inheritance without the problems class C extends SuperOfC with Trait1 with Trait2 { ... } A kind of abstract class such that: - evaluation of its constructor is pure: i.e.: - no side effects - no allocation of storage So this leads to the restrictions: ------------------------------------------ ... restrictions: 0. There is only one constructor 1. Constructor has no non-type parameters 2. All mixin base classes are traits 3. All parent class constructors have empty val parameter lists 4. Non-empty statements in the body are either: - imports, or - pure definitions More on "pure": The language specification (oct 15, 2005) says: "A pure definition can be evaluated without any side effect. Function, type, class , or object definitions are always pure. A value definition is pure if its right-hand side expression is pure. ... Pure expressions are paths, literals, and typed expressions e:T where e is pure" "statements" are imports, declarations, definitions, and expressions. ------------------------------------------ NOT ALLOWED IN TRAITS var definitions private var declarations val definitions that are not pure defs of secondary constructors ALLOWED IN A TRAIT val declarations (required values) val definitions that are pure var declarations (required fields) def declarations (abstract methods) def definitions (default methods) type declarations (required types) type definitions (provided types) object declarations imports packages ------------------------------------------ Q: Why are object declarations ok? They are lazily initialized package misc; trait BadTraits // (x: Int) // illegal, no non-type parameters! { val x: Int; val y: Int = 3; // val notPure = new Array[Int](3); // illegal, not pure! protected var z: Int; // var z = 0; // illegal, not a pure definition! // def this(x: Int) = { this(); 0 } // illegal, not pure! def f = { z = z+1; z } object Foo { def x = 3; } object Bar { var q = 7; } class Baz(i: Int) { var zz = 0; } object looksFishyButOk extends Baz(f); type X <: AnyRef; type B = Baz; } *** Semantics of classes, traits, and objects and packages **** sugars ------------------------------------------ SEMANTICS OF CLASSES AND OBJECTS Sugars for constructors class constructorName ... ==> class constructorName() ... class cn(..., val x: T, ...) ... { S1 ... Sn } ==> class cn(..., x': T, ...) ... { val x:T = x'; [x'/x]S1 ... [x'/x]Sn } Sugars for classes class id(args); ==> class id(args) {} class id(args) TemplateBody ==> class id(args) extends AnyRef with ScalaObject TemplateBody ------------------------------------------ ScalaObject is added as a mixin to all classes declared in Scala. **** semantics ------------------------------------------ SEMANTICS OF CLASS DEFINITIONS Environment = {types: [Id -> Type], values: [Id -> Value]} Type = {supers: SetOf(Id), valueArgs: Formal*} Value = Location M_C: ClassDef -> Environment -> Store -> Environment x Store M_C[[class id(x1:T1, ..., xn:Tn) extends scid(e1, ..., em) TemplateBody]] env s = (denv, s1) where denv.types(id) = { supers = { scid }, valueArgs = (x1:T1, ..., xn:Tn) } denv.values(id) = l where l not in dom(s) and s(l) = { class = env("metaclass"), super = env.values(scid), vtable = /* ... */, constr = let lsup = env(scid) in \(v1, ..., vn).\lnew.\s2. let env2 = { x1 = v1, ... xn = vn } (rands, s3) = M_E*[[e1,...,em]] (env2)s2 ((),s4) = lsup.constr(rands) (lnew)s3 s5 = s4.update(lnew, { x1 = v1, ..., xn = vn }) in M_E[[TemplateBody]] (env2) s5 } ------------------------------------------ Q: What's the vtable? Q: Why locations? Q: What does the constructor do? **** Mixins and diamond inheritance Consider the following... ------------------------------------------ package misc; object Diamond { trait Top { def m1 = "Top's m1"; def m2 = "Top's m2"; def m3 = "Top's m3"; def m4 = "Top's m4"; def m5 = "Top's m5"; def m6: String; def m7: String; def m8: String; } trait Left extends Top { override def m2 = "Left's m2"; abstract override def m3: String; override def m4 = "Left's m4"; override def m5 = "Left's m5"; def m6 = "Left's m6"; def m9 = "Left's m9"; } trait Right extends Top { override def m3 = "Right's m3"; abstract override def m4: String; override def m5 = "Right's m5"; def m7 = "Right's m7"; def m9 = "Right's m9"; } class Bottom extends Top with Left with Right { // must override m5 to resolve conflict override def m5 = "Bottom's m5"; // must provide required method m8 def m8 = "Bottom's m8"; // must resolve conflict on m9 override def m9 = super[Left].m9 + " + " + super[Right].m9; } class Bottom2 extends Left with Right { // no conflict on m5! // must provide required method m8 def m8 = "Bottom2's m8"; // Right's m9 would need override // without the following override def m9 = "Bottom2's m9"; } class Bottom3 extends Right with Left { // no conflict on m5! // must provide required method m8 def m8 = "Bottom2's m8"; // Left's m9 would need override // without the following override def m9 = "Bottom2's m9"; } def main(args: Array[String]) = { val b: Bottom = new Bottom(); Console.println("b.m1 = " + b.m1); Console.println("b.m2 = " + b.m2); Console.println("b.m3 = " + b.m3); Console.println("b.m4 = " + b.m4); Console.println("b.m5 = " + b.m5); Console.println("b.m6 = " + b.m6); Console.println("b.m7 = " + b.m7); Console.println("b.m8 = " + b.m8); Console.println("b.m9 = " + b.m9); Console.println("-----------------"); val b2: Bottom2 = new Bottom2(); Console.println("b2.m1 = " + b2.m1); Console.println("b2.m2 = " + b2.m2); Console.println("b2.m3 = " + b2.m3); Console.println("b2.m4 = " + b2.m4); Console.println("b2.m5 = " + b2.m5 + " (1)"); Console.println("b2.m6 = " + b2.m6); Console.println("b2.m7 = " + b2.m7); Console.println("b2.m8 = " + b2.m8); Console.println("b2.m9 = " + b2.m9); Console.println("-----------------"); val b3: Bottom3 = new Bottom3(); Console.println("b3.m1 = " + b3.m1); Console.println("b3.m2 = " + b3.m2); Console.println("b3.m3 = " + b3.m3); Console.println("b3.m4 = " + b3.m4); Console.println("b3.m5 = " + b3.m5 + " (2)"); Console.println("b3.m6 = " + b3.m6); Console.println("b3.m7 = " + b3.m7); Console.println("b3.m8 = " + b3.m8); Console.println("b3.m9 = " + b3.m9); } } ------------------------------------------ Q: Why are the given overrides needed in Bottom? Q: What is the output? b.m1 = Top's m1 b.m2 = Left's m2 b.m3 = Right's m3 b.m4 = Left's m4 b.m5 = Bottom's m5 b.m6 = Left's m6 b.m7 = Right's m7 b.m8 = Bottom's m8 b.m9 = Left's m9 + Right's m9 ----------------- b2.m1 = Top's m1 b2.m2 = Left's m2 b2.m3 = Right's m3 b2.m4 = Left's m4 b2.m5 = Right's m5 (1) b2.m6 = Left's m6 b2.m7 = Right's m7 b2.m8 = Bottom2's m8 b2.m9 = Bottom2's m9 ----------------- b3.m1 = Top's m1 b3.m2 = Left's m2 b3.m3 = Right's m3 b3.m4 = Left's m4 b3.m5 = Left's m5 (2) b3.m6 = Left's m6 b3.m7 = Right's m7 b3.m8 = Bottom2's m8 b3.m9 = Bottom2's m9 Compilation finished at Tue Nov 01 10:32:29 Q: What's the effect of putting a trait in extends rather than with? it gives it less precedence Q: So what's the rule? ------------------------------------------ RULES FOR OVERRIDES 1. Overrides in the body of the class win and can resolve conflicts 2. Unique definitions satisfy requirements 3. Overrides in mixins (with) have precedence over parents (extends) 4. Multiple overrides or definitions in mixins are have equal precedence and have to be resolved ------------------------------------------ **** Precedence and linearization Q: Is linearization needed to resolve precedence of overriding? Not sure... but think not. Linerization doesn't matter for construction, since traits can't have side effects in construction. **** abstract overrides and layered traits from An Overview of the Scala Programming Langauge, by Odersky et al. ------------------------------------------ PROVIDING OVERRIDES AS MIXINS trait Iterator[T] { def hasNext: Boolean; def next: T; } class BufferIterator[T] extends Iterator[T] { def hasNext: Boolean = ...; def next: T = ...; } trait SynchronizedIterator[T] extends Iterator[T] { abstract override def next: T = synchronized { super.next } abstract override def hasNext: Boolean = synchronized { super.hasNext } } class SynchBufferIterator[T] extends BufferIterator[T] with SynchronizedIterator[T]; ------------------------------------------ Note the use of "abstract override", this says that it's overriding both an abstract method and when mixed in it will override a concrete method also.