CSE 130: Fall 2012

Generics: Subtyping & Parametric Polymorphism

Ranjit Jhala, UC San Diego

Today's Plan: A Tale of Two Polymorphisms

Polymorphism Enables Reuse

A Tale of Two Polymorphisms

Today's Plan: A Tale of Two Polymorphisms

  1. Parametric Polymorphism ("type variables", ML style)
  1. Subtyping + Parametric Polymorphism
  1. Implicitly retrofitting behavior to existing types

Parametric Polymorphism in Scala

Parametric Polymorphism in Scala

Much like ML (and Java/C# Generics)

Except that you have to write down the type variables

In Ocaml

let id x = x 

In Scala

def id[A](x: A): A = x

Parametric Polymorphism in Scala

Much like ML (and Java/C# Generics)

Except that you have to write down the type variables

In Ocaml

val swap : ('a * 'b) -> ('b * 'a)
let swap p = (snd p, fst p)

In Scala

def swap[A, B](p: (A, B)) : (B, A) = (p._2, p._1)

Parametric Polymorphism in Scala

Much like ML (and Java/C# Generics)

Except that you have to write down the type variables

In Ocaml

let rec length xs = match xs with
  | []   -> 0
  | _::t -> 1 + length t

In Scala

def length[A](xs: List[A]): Int = xs match {
    case Nil      => 0
    case (_ :: t) => 1 + length(t)
  }

Parametric Polymorphism in Scala

Much like ML (and Java/C# Generics)

Except that you have to write down the type variables

In Ocaml

let rec map f xs = match xs with
  | []   -> [] 
  | x::t -> (f x) :: map (f, t) 

In Scala

def map[A, B](f: A => B)(xs: List[A]): List[B] = xs match {
    case Nil      => List() 
    case (_ :: t) => (f(x))::(map(f)(rest))
  }

QUIZ: Are these two functions equivalent?

//Subtype Polymorphism
def headAny(xs: List[Any]): Any = xs match { 
  case h :: _ => h 
}

//Parametric Polymorphism
def headPoly[A](xs: List[A]): A = xs match { 
  case h :: _ => h 
}

A. Yes

B. No because headAny accepts more inputs

C. No because headPoly accepts more inputs

Parametric Polymorphism in Scala

Parametric Polymorphism in Scala

Lets write a polymorphic library for representing (cough Sets!)

sealed abstract class Bag[A] 
case class Empty[A]()                    extends Bag[A]
case class Plus[A](elt: A, rest: Bag[A]) extends Bag[A]

The above is very similar to what you might write in Ocaml

type 'a bag = Empty | Plus of ('a * 'a bag)

Parametric Polymorphism in Scala

Lets write a polymorphic library for representing (cough Sets!)

sealed abstract class Bag[A]
case class Empty[A]()                    extends Bag[A]
case class Plus[A](elt: A, rest: Bag[A]) extends Bag[A]

Case Class Are Just Vanilla Classes

Parametric Polymorphism in Scala

Lets write a polymorphic library for representing (cough Sets!)

sealed abstract class Bag[A]
case class Empty[A]()                    extends Bag[A]
case class Plus[A](elt: A, rest: Bag[A]) extends Bag[A]

Parametric Polymorphism in Scala

Lets write a polymorphic library for representing (cough Sets!)

We can fill in various methods...

sealed abstract class Bag[A] {
  def size : Int = 
    this match {
      case Empty()       => 0
      case Plus(_, rest) => 1 + rest.size
    }
  // ...
}

Parametric Polymorphism in Scala

Lets write a polymorphic library for representing (cough Sets!)

We can fill in various methods...

sealed abstract class Bag[A] {
  // ...
  def contains(x: A) : Boolean = {
    this match {
      case Empty()                => false
      case Plus(e, _) if (x == e) => true  
      case Plus(_, rest)          => rest.contains(x) 
    }
  }
  // ...
}

Parametric Polymorphism in Scala

Lets write a polymorphic library for representing (cough Sets!)

We can fill in various methods...

sealed abstract class Bag[A] {
  // ...
  def add(x: A) : Bag[A] = {
    if (this.contains(x)) this else { Plus(x, this) }
  }
  // ...
}

Note Type parameter A is in scope in entire class definition

Parametric Polymorphism in Scala

Lets write a polymorphic library for representing (cough Sets!)

We can fill in various methods...

sealed abstract class Bag[A] {
  // ...
  def gimme: Option[A] = 
    this match {
      case Plus(x, rest) => Some(x)
      case _             => None
    }
  // ...
}

Note Type parameter A is in scope in entire class definition

Go To Code

Today's Plan: A Tale of Two Polymorphisms

  1. Parametric Polymorphism ("type variables", ML style)

  2. Subtyping + Parametric Polymorphism

  3. Implicitly retrofitting behavior to existing types

QUIZ: What is the value of res?

def findMin[A](cur: A, xs: List[A]): A = 
  xs match { 
    case Nil               => cur
    case h::t if (h < cur) => findMin(h, t) 
    case _::t              => findMin(cur, t)
  }

val res = findMin(1000, List(20, 4, 1, 6))

A. Type Error in findMin(1000, ...)

B. Type Error in definition of findMin

C. Type Error in List(20,4,1,6)

D. 1

E. 20

Bag is a pretty lame data structure

Must walk over entire bag to determine if elements are present.

An Ordered Bag

Lets change contains to utilize ordering property:

sealed abstract class Bag[A] {
  // ...
  def contains(x: A) : Boolean = {
    this match {
      case Empty()                => false
      case Plus(e, _) if (x == e) => true  
      case Plus(e, _) if (x  > e) => false 
      case Plus(_, rest)          => rest.contains(x) 
    }
  }
  // ...
}

Compiler is not happy

OrderedBag.scala:16: error: value > is not a member of type parameter A
      case Plus(e, _) if (x  > e) => false 

Compiler is not happy ... as we've made a mistake!!!

OrderedBag.scala:16: error: value > is not a member of type parameter A
      case Plus(e, _) if (x  > e) => false 

Remember: Types Are Contracts ...

sealed abstract class Bag[A] {
  def contains(x: A) : Boolean = {
    this match {
      case Plus(e, _) if (x  > e) => false 
    }
  }
}

Compiler is not happy ... as we've made a mistake!!!

OrderedBag.scala:16: error: value > is not a member of type parameter A
      case Plus(e, _) if (x  > e) => false 

Remember: Types Are Contracts ...

Need to describe types supporting comparison...

A Trait Describing Types Supporting Comparison

Ord.scala

trait Ord[A] {
  //Must Be Implemented
  def cmp(that: A): Int 
  
  //Automatically derived from cmp 
  def ===(that: A): Boolean = (this cmp that) == 0
  def <(that: A): Boolean   = (this cmp that) < 0
  def >(that: A): Boolean   = (this cmp that) > 0
  def <=(that: A): Boolean  = (this cmp that) <= 0
  def >=(that: A): Boolean  = (this cmp that) <= 0
}

Very much like Java interfaces

An Ordered Bag

sealed abstract class Bag[A <: Ord[A]] {
  //...
}

Using trait Ord to State Requirements

An Ordered Bag

We can do comparisons on A in contains

Now we can go ahead and do comparisons on A

sealed abstract class Bag[A <: Ord[A]] {
  // ...
  def contains(x: A) : Boolean = {
    this match {
      case Empty()                => false
      case Plus(e, _) if (x == e) => true  
      case Plus(_, rest)          => rest.contains(x) 
    }
  }
  // ...
}

An Ordered Bag

Change the add method so objects are put in the right position

sealed abstract class Bag[A <: Ord[A]] {
  //...
  def add(x: A) : Bag[A] = 
    this match {
      case Plus(e, rest) if (x > e)  => Plus(e, rest.add(x))
      case Plus(e, _)    if (x == e) => this
      case _                         => Plus(x, this)
    }
  //...
}

An Ordered Bag

Change the remove method to make it more efficient

No need to go to the end

sealed abstract class Bag[A <: Ord[A]] {
  def remove(x: A) : Bag[A] = {
    this match {
      case Plus(e, rest) if (x <  e) => this // no need to look further
      case Empty()                   => this
      case Plus(e, rest) if (x == e) => rest 
      case Plus(e, rest)             => Plus(e, rest.remove(x))
    }
  }
}

Go To Code

Recap: Subtyping + Parametric Polymorphism

Define function, class, etc. for any type A that is a subtype of something

sealed abstract class Bag[A <: Ord[A]] {
  //...
}

Bounded Quantification

QUIZ: What is the value of res?

def findMin[A <: Ord[A]](cur: A, xs: List[A]): A = 
  xs match { 
    case Nil               => cur
    case h::t if (h < cur) => findMin(h, t) 
    case _::t              => findMin(cur, t)
  }

val res = findMin(1000, List(20, 4, 1, 6))

A. Type Error in findMin(1000, ...)

B. Type Error (in definition of findMin)

C. Type Error (in List(20, 4, 1, 6))

D. 1

E. 20

How to add functionality to existing types?

Using FixedOrderedBag.scala we get

$ make fobag
$ scala

scala> Bag(1,2,3)
<console>:8: error: inferred type arguments [Int] do not conform to 
method apply's type parameter bounds [A <: Ord[A]]
       Bag(1,2,3)

How to add functionality to existing types?

Using FixedOrderedBag.scala we get

$ make fobag
$ scala

scala> Bag(1,2,3)
<console>:8: error: inferred type arguments [Int] do not conform to 
method apply's type parameter bounds [A <: Ord[A]]
       Bag(1,2,3)

Cannot always predict future uses of a type

Today's Plan: A Tale of Two Polymorphisms

  1. Parametric Polymorphism ("type variables", ML style)

  2. Subtyping + Parametric Polymorphism

  3. Implicitly retrofitting behavior to existing types

How to add functionality to existing types?

Cannot always predict future uses of a type

Retrofitting Functionality with Proxies

def findMin[A](cur: A, xs: List[A])(proxy: A => Ord[A]): A = 
  xs match { 
    case Nil                      => cur
    case h::t if (proxy(h) < cur) => findMin(h, t)(proxy) 
    case _::t                     => findMin(cur, t)(proxy)
  }

Retrofitting Functionality with Proxies

def findMin[A](cur: A, xs: List[A])(proxy: A => Ord[A]): A = 
  xs match { 
    case Nil                      => cur
    case h::t if (proxy(h) < cur) => findMin(h, t) 
    case _::t                     => findMin(cur, t)
  }

Creating a Proxy Function

def intProxy(x: Int) = new Ord[Int] {
  def cmp(that: Int) : Int = x - that
}

Retrofitting Functionality with Proxies

def findMin[A](cur: A, xs: List[A])(proxy: A => Ord[A]): A = 
  xs match { 
    case Nil                      => cur
    case h::t if (proxy(h) < cur) => findMin(h, t)(proxy) 
    case _::t                     => findMin(cur, t)(proxy)
  }

Creating A Proxy Function

def intProxy(x: Int) = new Ord[Int] {
  def cmp(that: Int) : Int = x - that
}

Using A Proxy Function

scala> import ProxyDemo._ 
scala> findMin(1000, List(20, 4, 1, 6))(intProxy)
res1: Int = 1

Retrofitting Functionality with Proxies

Create Proxies For Other Types Too...

def stringProxy(x: String) = new Ord[String] {
  def cmp(that: String) : Int = x compare that
}

Using The Proxy Function

scala> import ProxyDemo._ 
scala> findMin("z", List("apple", "chorizo", "machaca", "adobado"))(stringProxy)
res1: String = adobado 

Retrofitting Functionality with Proxies

Proxies are awesome ...

... but Proxies are also Lame

Strategy (for life!): focus on awesome, cut the lame...

Implicit Parameters and Values

Mark certain function params as implicit

def sayHello(implicit n: String) = println("Hello " + n) 

Implicit Parameters and Values

Mark certain function params as implicit

def sayHello(implicit n: String) = println("Hello " + n) 

Mark certain values as implicit

implicit val defaultName = "Ranjit"

Implicit Parameters and Values

Mark certain function params as implicit

def sayHello(implicit n: String) = println("Hello " + n) 

Mark certain values as implicit

implicit val defaultName = "Ranjit"

Can call the functions in the usual way

scala> sayHello("Roberto")
Hello Roberto

Implicit Parameters and Values

Mark certain function params as implicit

def sayHello(implicit n: String) = println("Hello " + n) 

Mark certain values as implicit

implicit val defaultName = "Ranjit"

Can call the functions in the usual way

scala> sayHello("Roberto")
Hello Roberto

... otherwise Scala fills in the blanks!

scala> sayHello
Hello Ranjit

Implicits: The Magic of Types

Scala uses types to fill in the blanks

Implicit Conversions: Automatically "Fix" Type Errors

Suppose I wrote a function expecting String inputs

scala> def yu(msg: String) = println("Y U  NO " + msg)
yu: (msg: String)Unit

Implicit Conversions: Automatically "Fix" Type Errors

Suppose I wrote a function expecting String inputs

scala> def yu(msg: String) = println("Y U  NO " + msg)
yu: (msg: String)Unit

Naturally, this is fine ...

scala> yu("GIVE EASY FINAL")
Y U  NO GIVE EASY FINAL

Implicit Conversions: Automatically "Fix" Type Errors

Suppose I wrote a function expecting String inputs

scala> def yu(msg: String) = println("Y U  NO " + msg)
yu: (msg: String)Unit

Naturally, this is fine ...

scala> yu("GIVE EASY FINAL")
Y U  NO GIVE EASY FINAL

... but this throws an error

scala> yu(10)
<console>:9: error: type mismatch;
 found   : Int(10)
 required: String
       yu(10)
          ^

Implicit Conversions: Automatically "Fix" Type Errors

Suppose I wrote a function expecting String inputs

scala> def yu(msg: String) = println("Y U  NO " + msg)
yu: (msg: String)Unit

But if we add an implicit conversion

scala> implicit def i2s(i: Int) = i.toString
i2s: (i: Int)java.lang.String

Implicit Conversions: Automatically "Fix" Type Errors

Suppose I wrote a function expecting String inputs

scala> def yu(msg: String) = println("Y U  NO " + msg)
yu: (msg: String)Unit

But if we add an implicit conversion

scala> implicit def i2s(i: Int) = i.toString
i2s: (i: Int)java.lang.String

Then lo and behold!

scala> yu(10)       
Y U  NO 10

Retrofitting Functionality with Proxies

Retrofitting Functionality with Implicit Proxies

Step 1: Make proxy parameters implicit

def findMin[A](cur: A, xs: List[A])(implicit proxy: A => Ord[A]): A = 
  xs match { 
     case Nil                      => cur
     case h::t if (proxy(h) < cur) => findMin(h, t)(proxy) 
     case _::t                     => findMin(cur, t)(proxy)
  }

Retrofitting Functionality with Implicit Proxies

Step 2: Remove explicit uses of proxy (Scala automatically inserts them!)

def findMin[A](cur: A, xs: List[A])(implicit proxy: A => Ord[A]): A = 
  xs match { 
     case Nil                        => cur
     case h::t if (/*proxy*/h < cur) => findMin(h, t)/* (proxy) */ 
     case _::t                       => findMin(cur, t)/* (proxy) */
  }

Retrofitting Functionality with Implicit Proxies

Step 2: Remove explicit uses of proxy (Scala automatically inserts them!)

def findMin[A](cur: A, xs: List[A])(implicit proxy: A => Ord[A]): A = 
  xs match { 
     case Nil               => cur
     case h::t if (h < cur) => findMin(h, t) 
     case _::t              => findMin(cur, t)
  }

Retrofitting Functionality with Implicit Proxies

Step 3: Make proxy functions implicit

  implicit def intProxy(x: Int) = new Ord[Int] {
    def cmp(that: Int) : Int = x - that
  }
 
  implicit def stringProxy(x: String) = new Ord[String] {
    def cmp(that: String) : Int = x compare that
  }

Retrofitting Functionality with Implicit Proxies

After steps

  1. Make proxy parameters implicit

  2. Remove explicit uses of proxy (Scala automatically inserts them!)

  3. Make proxy functions implicit

Retrofitting Functionality with Implicit Proxies

After steps

  1. Make proxy parameters implicit

  2. Remove explicit uses of proxy (Scala automatically inserts them!)

  3. Make proxy functions implicit

We get:

scala> val res = findMin(1000, List(10, 2, 30, 14)) // look ma, no proxies!
res: Int = 2

Retrofitting Functionality with Implicit Proxies

This is a common enough pattern to warrant special syntax.

Instead of:

def findMin[A](cur: A, xs: List[A])(implicit proxy: A => Ord[A]): A

we can just write the equivalent

def findMin[A <% Ord[A]](cur: A, xs: List[A]): A

Retrofitting Functionality with Implicit Proxies

This is a common enough pattern to warrant special syntax.

Instead of:

def findMin[A](cur: A, xs: List[A])(implicit proxy: A => Ord[A]): A

we can just write the equivalent

def findMin[A <% Ord[A]](cur: A, xs: List[A]): A

View Bound

Retrofitting Functionality with Implicit Proxies

We can now make a real ordered bag

sealed abstract class Bag[A <% Ord[A]] {

  def contains(x: A) : Boolean = {
    this match {
      case Empty()                => false
      case Plus(e, _) if (x == e) => true  
      case Plus(e, _) if (x  > e) => false 
      case Plus(_, rest)          => rest.contains(x) 
    }
  }

  // etc.

Retrofitting Functionality with Implicit Proxies

We can now make a real ordered bag

We make a companion object that makes it easy to create Bags

/* A Companion Object that has various key methods */
object Bag {
  def apply[A <% Ord[A]](xs: A*) : Bag[A] = { 
    val s0 : Bag[A] = Empty()
    xs.toList.foldLeft(s0)(_ add _)  
  }
}

Note: the view bound on apply

Retrofitting Functionality with Implicit Proxies

We can now make a real ordered bag

We make a companion object that makes it easy to create Bags

We can finally use Bag with existing types!

scala> val b0 = Bag(3,1,4,2)
<console>:7: error: No implicit view available from Int => Ord[Int].
       val b0 = Bag(3,1,4,2)
                   ^

Retrofitting Functionality with Implicit Proxies

First, lets import the proxies into scope ...

scala> import OrdInstances._

Retrofitting Functionality with Implicit Proxies

First, lets import the proxies into scope ...

scala> import OrdInstances._

And we're off...

scala> val b0 = Bag(3,1,2) // Now it can insert the intProxy 
b0: Bag[Int] = 1,2,3,4     // Note how the Bag is ordered

scala> b0 gimme
res0: Option[Int] = Some(1)

scala> b0 remove 1 gimme
res1: Option[Int] = Some(2)

scala> b0 remove 1 remove 2 gimme
res2: Option[Int] = Some(3)

scala> b0 remove 1 remove 2 remove 3 gimme
res3: Option[Int] = None

Retrofitting Functionality with Implicit Proxies

We can use any type for which an implicit proxy is defined:

scala> val b1 = Bag("donkey", "monkey", "cat")
b1: Bag[java.lang.String] = cat,donkey,monkey

scala> b1 contains "monkey"
res: Boolean = true

scala> b1 contains "hippo"
res: Boolean = false 

scala> val b2 = b1 add "hippo"
b2: Bag[java.lang.String] = cat,donkey,hippo,monkey

scala> b2 contains "hippo"
res: Boolean = true

Retrofitting Functionality with Implicit Proxies

The Coolest Part: Bootstrapping proxies

Scala will automatically generate new proxies (if we teach it how)

implicit def tup2Proxy[A <% Ord[A], B <% Ord[B]](x: (A, B)) 
  = new Ord[(A, B)] {
      def cmp(that: (A, B)) : Int = { 
        val c1 = x._1 cmp that._1
        if (c1 != 0) c1 else { x._2 cmp that._2 } 
      }
    }

Retrofitting Functionality with Implicit Proxies

The Coolest Part: Bootstrapping proxies

Scala will automatically generate new proxies (if we teach it how)

implicit def tup2Proxy[A <% Ord[A], B <% Ord[B]](x: (A, B)) 
  = new Ord[(A, B)] {
      def cmp(that: (A, B)) : Int = { 
        val c1 = x._1 cmp that._1
        if (c1 != 0) c1 else { x._2 cmp that._2 } 
      }
    }

Means:

Retrofitting Functionality with Implicit Proxies

The Coolest Part: Bootstrapping proxies

And now we can make Bags of all sorts of pairs (over Int and String)

scala> val tb = Bag((3, "cat"), (2, "horse"), (1, "zebra"), (0, "giraffe"))
tb: Bag[(Int, java.lang.String)] = (0,giraffe),(1,zebra),(2,horse),(3,cat)

scala> val tb = Bag((2, (1, "cat")), (30, (20, "horse")), (11, (7, "zebra")))
tb: Bag[(Int, (Int, java.lang.String))] = (2,(1,cat)),(11,(7,zebra)),(30,(20,horse))

Today's Plan: A Tale of Two Polymorphisms

  1. Parametric Polymorphism ("type variables", ML style)

    • def add[A](x: A): Bag[A]
  2. Subtyping + Parametric Polymorphism

    • def add[A <: Ord[A]](x: A): Bag[A]
  3. Implicitly retrofitting behavior to existing types

    • def add[A <% Ord[A]](x: A): Bag[A]

P.S.: Implicits in PA6 Json Conversion

Next Time: Prolog...