TI1220 Lecture 8: Traits & Type Parameterization

Preview:

Citation preview

TI1220 2012-2013Concepts of Programming Languages

Eelco Visser / TU Delft

Lecture 8: Traits & Type Parameterization

var ms := course.managers;

var ms : Set<Person> := Set<Person>();ms.addAll(course.managers);

Analysis => Lecture 12: Concurrency

Root Cause

The Fix

Intention: copy semantics

Effect: reference semantics

The Fault: Concurrent writes on CourseEdition

The Airconditioning

Traits

abstract class Element { def contents: Array[String] def height: Int = contents.length def width: Int = if (height == 0) 0 else contents(0).length}

class ArrayElement(conts: Array[String]) extends Element { val contents: Array[String] = conts}

Classical Inheritance

Inheriting fields and methods

class UniformElement( ch: Char, override val width: Int, override val height: Int) extends Element { private val line = ch.toString * width def contents = Array.make(height, line)}

val e1: Element = new ArrayElement(Array("hello", "world"))val ae: ArrayElement = new LineElement("hello")val e2: Element = aeval e3: Element = new UniformElement('x', 2, 3)

Subtyping

abstract class Element { def demo() { println("Element's implementation invoked") }}class ArrayElement extends Element { override def demo() { println("ArrayElement's implementation invoked") }}class LineElement extends ArrayElement { override def demo() { println("LineElement's implementation invoked") }}// UniformElement inherits Element’s democlass UniformElement extends Element

def invokeDemo(e: Element) { e.demo()}scala> invokeDemo(new ArrayElement)ArrayElement's implementation invoked

scala> invokeDemo(new LineElement)LineElement's implementation invoked

scala> invokeDemo(new UniformElement)Element's implementation invoked

Dynamic Binding

Subtyping

• Polymorphism & dynamic binding

Code Reuse

• reuse instance variables and methods from super class

Single Inheritance

• cannot reuse code from more than one class

Interfaces

• support subtyping multiple classes

• must re-implement interface

Java-style Single Inheritance

Trait

• reusable unit of code

• encapsulates method and field definitions

• reused by mixing into classes

• class can mix in any number of traits

Applications

• rich interfaces

• stackable modifications

trait Philosophical { def philosophize() { println("I consume memory, therefore I am!") }}class Frog extends Philosophical { override def toString = "green"}

scala> val frog = new Frogfrog: Frog = greenscala> frog.philosophize()I consume memory, therefore I am!

scala> val phil: Philosophical = frogphil: Philosophical = greenscala> phil.philosophize()I consume memory, therefore I am!

inheritance: code reuse subtyping: traits are types

Defining and Using Traits

class Animaltrait HasLegsclass Frog extends Animal with Philosophical with HasLegs { override def toString = "green" override def philosophize() { println("It ain't easy being " + toString + "!") }}

superclasstraits

override code from traitscala> val phrog: Philosophical = new Frogphrog: Philosophical = greenscala> phrog.philosophize()It ain't easy being green!

Mixing in (multiple) traits

Trait is like Java interface with

• methods

• fields

• state

Trait is Scala class

• without class parameters

• dynamic binding of ‘super’

Rich interface

• many methods

• convenient for client

• more work for implementer

Thin interface

• few methods

• easy for implementers

• inconvenient for client

trait CharSequence { def charAt(index: Int): Char def length: Int def subSequence(start: Int, end: Int): CharSequence def toString(): String}

Rich Interfaces with Traits

• small number of abstract methods implemented by client

• large number of concrete methods inherited by client

class Point(val x: Int, val y: Int)class Rectangle(val topLeft: Point, val bottomRight: Point) { def left = topLeft.x def right = bottomRight.x def width = right - left // and many more geometric methods...}

abstract class Component { def topLeft: Point def bottomRight: Point def left = topLeft.x def right = bottomRight.x def width = right - left // and many more geometric methods...}

Rectangular objects without traits

trait Rectangular { def topLeft: Point def bottomRight: Point def left = topLeft.x def right = bottomRight.x def width = right - left // and many more geometric methods...}

abstract class Component extends Rectangular { // other methods...}

class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular { // other methods...}

Rectangular objects with traits

trait Rectangular { def topLeft: Point def bottomRight: Point def left = topLeft.x def right = bottomRight.x def width = right - left // and many more geometric methods...}

abstract class Component extends Rectangular { // other methods...}

class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular { // other methods...}

scala> val rect = new Rectangle(new Point(1, 1), new Point(10, 10))rect: Rectangle = Rectangle@3536fdscala> rect.leftres2: Int = 1scala> rect.rightres3: Int = 10

Rectangular objects with traits

class Rational(n: Int, d: Int) { // ... def <(that: Rational) = this.numer * that.denom > that.numer * this.denom def >(that: Rational) = that < this def <=(that: Rational) = (this < that) || (this == that) def >=(that: Rational) = (this > that) || (this == that)}

defined in terms of < based on standard

semantics of ordering

{

A Rich Interface for Ordering

class Rational(n: Int, d: Int) { // ... def <(that: Rational) = this.numer * that.denom > that.numer * this.denom def >(that: Rational) = that < this def <=(that: Rational) = (this < that) || (this == that) def >=(that: Rational) = (this > that) || (this == that)}

defined in terms of < based on standard

semantics of ordering

{

class Rational(n: Int, d: Int) extends Ordered[Rational] { // ... def compare(that: Rational) = (this.numer * that.denom) - (that.numer * this.denom)}

A Rich Interface for Ordering

class Rational(n: Int, d: Int) { // ... def <(that: Rational) = this.numer * that.denom > that.numer * this.denom def >(that: Rational) = that < this def <=(that: Rational) = (this < that) || (this == that) def >=(that: Rational) = (this > that) || (this == that)}

defined in terms of < based on standard

semantics of ordering

{

class Rational(n: Int, d: Int) extends Ordered[Rational] { // ... def compare(that: Rational) = (this.numer * that.denom) - (that.numer * this.denom)}

Ordered trait provides reusable implementation of ordering

A Rich Interface for Ordering

Class Queue of integers

• put: place integer in queue

• get: take integer out

• first-in first-out

Modifications

• Doubling: double all integers put in queue

• Incrementing: increment all integers put in queue

• Filtering: filter out negative

Stackable Modifications

abstract class IntQueue { def get(): Int def put(x: Int)}

import scala.collection.mutable.ArrayBufferclass BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] def get() = buf.remove(0) def put(x: Int) { buf += x }}

Class Queue

abstract class IntQueue { def get(): Int def put(x: Int)}

import scala.collection.mutable.ArrayBufferclass BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] def get() = buf.remove(0) def put(x: Int) { buf += x }}

scala> val queue = new BasicIntQueuequeue: BasicIntQueue = BasicIntQueue@24655f

scala> queue.put(10)

scala> queue.put(20)

scala> queue.get()res9: Int = 10

scala> queue.get()res10: Int = 20

Class Queue

trait Doubling extends IntQueue { abstract override def put(x: Int) { super.put(2 * x) }}

dynamically bound

can only be mixed into subclasses of IntQueue

mix into class with concrete

definition

Trait Doubling

trait Doubling extends IntQueue { abstract override def put(x: Int) { super.put(2 * x) }}

dynamically bound

can only be mixed into subclasses of IntQueue

mix into class with concrete

definition

scala> class MyQueue extends BasicIntQueue with Doublingdefined class MyQueue

scala> val queue = new MyQueuequeue: MyQueue = MyQueue@91f017

scala> queue.put(10)

scala> queue.get()res12: Int = 20

Trait Doubling

trait Doubling extends IntQueue { abstract override def put(x: Int) { super.put(2 * x) }}

dynamically bound

can only be mixed into subclasses of IntQueue

mix into class with concrete

definition

scala> class MyQueue extends BasicIntQueue with Doublingdefined class MyQueue

scala> val queue = new MyQueuequeue: MyQueue = MyQueue@91f017

scala> queue.put(10)

scala> queue.get()res12: Int = 20

scala> val queue = new BasicIntQueue with Doublingqueue: BasicIntQueue with Doubling = $anon$1@5fa12d

scala> queue.put(10)

scala> queue.get()res14: Int = 20

Trait Doubling

trait Incrementing extends IntQueue { abstract override def put(x: Int) { super.put(x + 1) }}trait Filtering extends IntQueue { abstract override def put(x: Int) { if (x >= 0) super.put(x) }}

Stacking Modifications

trait Incrementing extends IntQueue { abstract override def put(x: Int) { super.put(x + 1) }}trait Filtering extends IntQueue { abstract override def put(x: Int) { if (x >= 0) super.put(x) }}

scala> val queue = (new BasicIntQueue with Incrementing with Filtering)scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get()res15: Int = 1scala> queue.get()res15: Int = 2

Stacking Modifications

trait Incrementing extends IntQueue { abstract override def put(x: Int) { super.put(x + 1) }}trait Filtering extends IntQueue { abstract override def put(x: Int) { if (x >= 0) super.put(x) }}

scala> val queue = (new BasicIntQueue with Incrementing with Filtering)scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get()res15: Int = 1scala> queue.get()res15: Int = 2

Stacking Modifications

scala> val queue = (new BasicIntQueue with Filtering with Incrementing)scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get()res17: Int = 0scala> queue.get()res18: Int = 1scala> queue.get()res19: Int = 2

// Multiple inheritance thought experimentval q = new BasicIntQueue with Incrementing with Doublingq.put(42) // which put would be called?

Multiple Inheritance (Why Not?)

Incrementing Doubling

BasicIntQueue

new BasicIntQueue with Increment with Doubling

// Multiple inheritance thought experimenttrait MyQueue extends BasicIntQueue with Incrementing with Doubling { def put(x: Int) { Incrementing.super.put(x) // (Not real Scala) Doubling.super.put(x) }}

Multiple Inheritance (Why Not?)

Incrementing Doubling

BasicIntQueue

new BasicIntQueue with Increment with Doubling

put of BasicIntQue called twice!

a class is always linearized before all of its superclasses and mixed in traits

class Animaltrait Furry extends Animaltrait HasLegs extends Animaltrait FourLegged extends HasLegsclass Cat extends Animal with Furry with FourLegged

Linearly Ordering Traits

Units of code

• reusable through inheritance

• can be mixed in at multiple places in hierarchy

Multiple inheritance ++

• calls to super are linearized

• avoid diamond problem

• stack changes

Traits Summary

What is the value of question in:

class Animal { override def toString = "Animal"}trait Furry extends Animal { override def toString = "Furry -> " + super.toString}trait HasLegs extends Animal { override def toString = "HasLegs -> " + super.toString}trait FourLegged extends HasLegs { override def toString = "FourLegged -> " + super.toString}class Cat extends Animal with Furry with FourLegged { override def toString = "Cat -> " + super.toString}val question = new Cat

a) Cat -> FourLegged -> HasLegs -> Furry -> Animalb) Cat -> HasLegs -> FourLegged -> Furry -> Animalc) Cat -> Furry -> FourLegged -> HasLegs -> Animald) Cat -> Furry -> HasLegs -> FourLegged -> Animal

Traits Experiment

Type Parameterization

def append[T](xs: List[T], ys: List[T]): List[T] = xs match { case List() => ys case x :: xs1 => x :: append(xs1, ys) }

def map[A,B](xs: List[A], f: A => B): List[B] = xs match { case List() => List() case y :: ys => f(y) :: map(ys, f)}

Generic classes and traits

• Set[T]: generic sets parameterized with type T

• Set[Int]: set of integers, instance of Set[T]

• No raw types: always use with type parameter

Example: Functional Queues

typedef struct queue_elem { int val; struct queue_elem *next;} queue_elem;

typedef struct queue { queue_elem *first; queue_elem *last;} queue;

queue *newQueue() { queue *q = (queue *)malloc(sizeof(queue)); q->first = NULL; q->last = NULL; return q;}

Imperative Queue in C

void enqueue(queue *q, int val) { queue_elem *elem = (queue_elem *)malloc(sizeof(queue_elem)); elem->val = val; elem->next = NULL; if(q->first == NULL) { q->first = q->last = elem; } else { q->last->next = elem; q->last = elem; }}

Imperative Queue in C

int dequeue(queue *q) { if(q == NULL || q->first == NULL) { return 0; } int val = q->first->val; queue_elem *elem = q->first; if(q->first == q->last) { q->last = NULL; } q->first = q->first->next; free(elem); return val;}

Imperative Queue in C

Queue operations

• head: return first element

• tail: return rest

• append: new queue with new element at the end

Functional Queue

• fully persistent

• contents not changed when appending

• efficient implementation should be O(1) for all operations

scala> val q = Queue(1, 2, 3)q: Queue[Int] = Queue(1, 2, 3)

scala> val q1 = q append 4q1: Queue[Int] = Queue(1, 2, 3, 4)

scala> qres0: Queue[Int] = Queue(1, 2, 3)

Functional Queue

class SlowAppendQueue[T](elems: List[T]) { // Not efficient def head = elems.head def tail = new SlowAppendQueue(elems.tail) def append(x: T) = new SlowAppendQueue(elems ::: List(x))}

Functional Queue (First Attempt)

class SlowAppendQueue[T](elems: List[T]) { // Not efficient def head = elems.head def tail = new SlowAppendQueue(elems.tail) def append(x: T) = new SlowAppendQueue(elems ::: List(x))}

append = O(n)

Functional Queue (First Attempt)

class SlowAppendQueue[T](elems: List[T]) { // Not efficient def head = elems.head def tail = new SlowAppendQueue(elems.tail) def append(x: T) = new SlowAppendQueue(elems ::: List(x))}

class SlowHeadQueue[T](smele: List[T]) { // Not efficient // smele is elems reversed def head = smele.last def tail = new SlowHeadQueue(smele.init) def append(x: T) = new SlowHeadQueue(x :: smele)}

append = O(n)

Functional Queue (First Attempt)

class SlowAppendQueue[T](elems: List[T]) { // Not efficient def head = elems.head def tail = new SlowAppendQueue(elems.tail) def append(x: T) = new SlowAppendQueue(elems ::: List(x))}

class SlowHeadQueue[T](smele: List[T]) { // Not efficient // smele is elems reversed def head = smele.last def tail = new SlowHeadQueue(smele.init) def append(x: T) = new SlowHeadQueue(x :: smele)}

append = O(n)

head, tail = O(n)

Functional Queue (First Attempt)

class SlowAppendQueue[T](elems: List[T]) { // Not efficient def head = elems.head def tail = new SlowAppendQueue(elems.tail) def append(x: T) = new SlowAppendQueue(elems ::: List(x))}

class SlowHeadQueue[T](smele: List[T]) { // Not efficient // smele is elems reversed def head = smele.last def tail = new SlowHeadQueue(smele.init) def append(x: T) = new SlowHeadQueue(x :: smele)}

append = O(n)

head, tail = O(n)

head, tail, append = O(1) cannot be possible!

Functional Queue (First Attempt)

class Queue[T]( private val leading: List[T], private val trailing: List[T]) { def head = leading.head def tail = new Queue(leading.tail, trailing) def append(x: T) = new Queue(leading, x :: trailing)}

elems == leading ::: trailing.reverse

Represent Queue with Two Lists

class Queue[T]( private val leading: List[T], private val trailing: List[T]) { def head = leading.head def tail = new Queue(leading.tail, trailing) def append(x: T) = new Queue(leading, x :: trailing)}

elems == leading ::: trailing.reverse

but what if leading.isEmpty?

Represent Queue with Two Lists

Mirroring

class Queue[T]( private val leading: List[T], private val trailing: List[T]) { private def mirror = if (leading.isEmpty) new Queue(trailing.reverse, Nil) else this def head = mirror.leading.head def tail = { val q = mirror new Queue(q.leading.tail, q.trailing) } def append(x: T) = new Queue(leading, x :: trailing)}

Mirroring

class Queue[T]( private val leading: List[T], private val trailing: List[T]) { private def mirror = if (leading.isEmpty) new Queue(trailing.reverse, Nil) else this def head = mirror.leading.head def tail = { val q = mirror new Queue(q.leading.tail, q.trailing) } def append(x: T) = new Queue(leading, x :: trailing)}

head, tail, append: O(1)mirror: O(n) but amortized over n calls of tail

Mirroring

class Queue[T]( private val leading: List[T], private val trailing: List[T]) { private def mirror = if (leading.isEmpty) new Queue(trailing.reverse, Nil) else this def head = mirror.leading.head def tail = { val q = mirror new Queue(q.leading.tail, q.trailing) } def append(x: T) = new Queue(leading, x :: trailing)}

head, tail, append: O(1)mirror: O(n) but amortized over n calls of tail

implementation is exposed!

class Queue[T] private ( private val leading: List[T], private val trailing: List[T]) { def this() = this(Nil, Nil) def this(elems: T*) = this(elems.toList, Nil)

def head = ... def tail = ... def append(x: T) = ...}

scala> Queue(1, 2, 3)

private parameters

public auxiliary constructors

hide implementation details from clients

Private Constructors

class Queue[T] private ( private val leading: List[T], private val trailing: List[T]) { def head = ... def tail = ... def append(x: T) = ... }

object Queue { // constructs a queue with initial elements ‘xs’ def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)}

Factory Method hide implementation details from clients

factory method

private parameters

trait Queue[T] { def head: T def tail: Queue[T] def append(x: T): Queue[T]}

object Queue { def apply[T](xs: T*): Queue[T] = new QueueImpl[T](xs.toList, Nil)

private class QueueImpl[T]( private val leading: List[T], private val trailing: List[T] ) extends Queue[T] { def mirror = if (leading.isEmpty) new QueueImpl(trailing.reverse, Nil) else this def head: T = mirror.leading.head def tail: QueueImpl[T] = { val q = mirror new QueueImpl(q.leading.tail, q.trailing) } def append(x: T) = new QueueImpl(leading, x :: trailing) }

}

hide implementation details from clients

scala> def doesNotCompile(q: Queue) {}<console>:5: error: trait Queue takes type parametersdef doesNotCompile(q: Queue) {}

scala> def doesCompile(q: Queue[AnyRef]) {}doesCompile: (Queue[AnyRef])Unit

Queue is a trait, not a typeQueue is a type constructor or generic trait

Queue[String] is a (specific) type

Generic Traits

Queue[String] subtype of Queue[AnyRef] ?

if S subtype of T then Queue[S] subtype of Queue[T] ?

If answer is yes: Queue is covariant in T

trait Queue[+T] { ... }

val q: Queue[AnyRef] = Queue[String](“a”)

If answer is no: Queue is contravariant in T

trait Queue[-T] { ... }

val q: Queue[String] = Queue[AnyRef]() default: nonvariant

Subtyping & Variance Annotations

class Cell[+T](init: T) { private[this] var current = init def get = current def set(x: T) { current = x }}

val c1 = new Cell[String]("abc")val c2: Cell[Any] = c1c2.set(1)val s: String = c1.get

Covariance and Mutable Classes

class Cell[+T](init: T) { private[this] var current = init def get = current def set(x: T) { current = x }}

val c1 = new Cell[String]("abc")val c2: Cell[Any] = c1c2.set(1)val s: String = c1.get

Cell.scala:7: error: covariant type T occurs incontravariant position in type T of value xdef set(x: T) = current = x

Covariance and Mutable Classes

package society { package professional { class Executive { private[professional] var workDetails = null private[society] var friends = null private[this] var secrets = null

def help(another : Executive) { println(another.workDetails) println(another.secrets) //ERROR } } }}

Source: http://www.tutorialspoint.com/scala/scala_access_modifiers.htm

Scope of Protection of Access Modifiers

A private member is visible only inside the class or object that contains the member definition.

A protected member is only accessible from subclasses of the class in which the member is defined.

Every member not labeled private or protected is public. There is no explicit modifier for public members. Such members can be accessed from anywhere.

// this is JavaString[] a1 = { "abc" };Object[] a2 = a1;a2[0] = new Integer(17);String s = a1[0];

no compile-time error

Variance and Arrays

// this is JavaString[] a1 = { "abc" };Object[] a2 = a1;a2[0] = new Integer(17);String s = a1[0];

Exception in thread "main" java.lang.ArrayStoreException:java.lang.Integer at JavaArrays.main(JavaArrays.java:8)

no compile-time error

Variance and Arrays

// this is JavaString[] a1 = { "abc" };Object[] a2 = a1;a2[0] = new Integer(17);String s = a1[0];

Exception in thread "main" java.lang.ArrayStoreException:java.lang.Integer at JavaArrays.main(JavaArrays.java:8)

no compile-time error

motivation: generic treatment of arrays:

void sort(Object[] a, Comparator cmp) { ... }

Variance and Arrays

scala> val a1 = Array("abc")a1: Array[java.lang.String] = Array(abc)

scala> val a2: Array[Any] = a1<console>:5: error: type mismatch; found : Array[java.lang.String] required: Array[Any] val a2: Array[Any] = a1 ˆ

scala> val a2: Array[Object] = a1.asInstanceOf[Array[Object]]a2: Array[java.lang.Object] = Array(abc)

Scala Arrays are Non-variant

class Queue[+T] { def append(x: T) = ...}

class StrangeIntQueue extends Queue[Int] { override def append(x: Int) = { println(Math.sqrt(x)) super.append(x) }}

val x: Queue[Any] = new StrangeIntQueuex.append("abc")

Checking Variance Annotations

class Queue[+T] { def append(x: T) = ...}

class StrangeIntQueue extends Queue[Int] { override def append(x: Int) = { println(Math.sqrt(x)) super.append(x) }}

val x: Queue[Any] = new StrangeIntQueuex.append("abc")

Queues.scala:11: error: covariant type T occurs incontravariant position in type T of value xdef append(x: T) = ˆ

Checking Variance Annotations

class Queue[+T]( private val leading: List[T], private val trailing: List[T]) { def append[U >: T](x: U) = new Queue[U](leading, x :: trailing) // ...}class Fruitclass Apple extends Fruitclass Orange extends Fruit

scala> val qa = Queue(new Apple)scala> val qb = qa.append(new Orange)qb: Queue[Fruit] = ...

U >: T == U is a supertype of T

Lower Bounds

class Queue[+T] private ( private[this] var leading: List[T], private[this] var trailing: List[T]) { private def mirror() = if (leading.isEmpty) { while (!trailing.isEmpty) { leading = trailing.head :: leading trailing = trailing.tail } } def head: T = { mirror(); leading.head } def tail: Queue[T] = { mirror(); new Queue(leading.tail, trailing) } def append[U >: T](x: U) = new Queue[U](leading, x :: trailing)}

Optimized Functional Queue

def orderedMergeSort[T <: Ordered[T]](xs: List[T]): List[T] = { def merge(xs: List[T], ys: List[T]): List[T] = (xs, ys) match { case (Nil, _) => ys case (_, Nil) => xs case (x :: xs1, y :: ys1) => if (x < y) x :: merge(xs1, ys) else y :: merge(xs, ys1) } val n = xs.length / 2 if (n == 0) xs else { val (ys, zs) = xs splitAt n merge(orderedMergeSort(ys), orderedMergeSort(zs)) }}

Upperbounds

Information hiding

• private constructors

• factory methods

• object private members

Type variance

• subtyping of generic types

• covariant, contravariant variance annotations

• lower bounds, upper bounds

Type Parameterization Summary

Reading & Programming in Week 6

Reading

Scala Chapter 12: Traits

Scala Chapter 19: Type Parameterization

Week 9: Parsers and Interpreters

WebLab: Graded Assignment 2: (deadline 14 May 2013, 23:59)

Recommended