- Subtyping Polymorphism ("is-a" style, classic OO)
- Parametric Polymorphism ("type variables", ML style)
- Scala combines them in interesting ways
- Parametric Polymorphism ("type variables", ML style)
- Subtyping + Parametric Polymorphism
- Implicitly retrofitting behavior to existing types
Functions
Data Types
Much like ML (and Java/C# Generics)
Except that you have to write down the type variables
let id x = x
def id[A](x: A): A = x
Much like ML (and Java/C# Generics)
Except that you have to write down the type variables
let swap p = (snd p, fst p)
def swap[A, B](p: (A, B)) = (p._2, p._1)
Much like ML (and Java/C# Generics)
Except that you have to write down the type variables
let rec length xs = match xs with
| [] -> 0
| _::t -> 1 + length t
def length[A](xs: List[A]): Int = xs match {
case Nil => 0
case (_ :: t) => 1 + length(t)
}
Much like ML (and Java/C# Generics)
Except that you have to write down the type variables
let rec map f xs = match xs with
| [] -> []
| x::t -> (f x) :: map (f, t)
def map[A, B](f: A => B)(xs: List[A]): List[B] = xs match {
case Nil => List()
case (_ :: t) => (f(x))::(map(f)(rest))
}
//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
D. No because headAny generates more outputs
E. No because headPoly generates more outputs
Functions
Data Types
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)
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]
- But no need for pesky
newto create instances- But also upporting pattern matching
- ML Data Constructors == OO Sub Classes
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]
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
}
// ...
}
- Note We use
thisto pattern match against the receiver bag
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)
}
}
// ...
}
- Note Type parameter
Ais in scope in entire class definition
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
- Same type of
Bagreturned as output...
- Immutable: returns a brand new
Bag
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
- Same kind of element returned as output...
- Immutable: element not removed from bag.
Parametric Polymorphism ("type variables", ML style)
Subtyping + Parametric Polymorphism
Implicitly retrofitting behavior to existing types
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 structurecontainsaddMust walk over entire bag to determine if elements are present.
- Let us arrange the elements in increasing order ...
- ... can tell if an element is absent when we find a bigger one.
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)
}
}
// ...
}
OrderedBag.scala:16: error: value > is not a member of type parameter A
case Plus(e, _) if (x > e) => false
OrderedBag.scala:16: error: value > is not a member of type parameter A
case Plus(e, _) if (x > e) => false
- Compiler says
containsrequires more properties fromA...- Cannot implement
containsfor every typeA...- ... only for types that support comparision
sealed abstract class Bag[A] {
def contains(x: A) : Boolean = {
this match {
case Plus(e, _) if (x > e) => false
}
}
}
OrderedBag.scala:16: error: value > is not a member of type parameter A
case Plus(e, _) if (x > e) => false
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
}
cmp ...sealed abstract class Bag[A <: Ord[A]] {
//...
}
Ord to State Requirements
- For every
Athat is a subtype ofOrd[A](i.e. that implementsOrd[A])
- We define the class and methods of
Bag[A]
A in containsNow 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)
}
}
// ...
}
add method so objects are put in the right positionsealed 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)
}
//...
}
remove method to make it more efficientNo 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))
}
}
}
Define function, class, etc. for any type A that is a subtype of something
sealed abstract class Bag[A <: Ord[A]] {
//...
}
A that satisfies a conditiondef 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
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)
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)
- We just made up a brand new trait
Ord...
- ... so of course the existing type
Intdoes not implementOrd!!!
- Could make our own re-implementations of
Intbut thats lame...
Parametric Polymorphism ("type variables", ML style)
Subtyping + Parametric Polymorphism
Implicitly retrofitting behavior to existing types
We just made up a brand new trait Ord ...
... so of course the existing type Int does not implement Ord!
- Solution: Use a proxy that does the comparison
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)
}
- Now, the
Intneed not itself implementOrd[Int]
- i.e.
Intneed not support comparisons withInt
- But should be some proxy that is able to do comparisons on its behalf...
proxy(h) < curinstead ofh < cur
- Next, lets create such a proxy function!
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)
}
def intProxy(x: Int) = new Ord[Int] {
def cmp(that: Int) : Int = x - that
}
- Proxy maps
x: Intto anOrd[Int]supporting comparisons withx
- Thus
proxy(h) < curwill hold exactly whenhis less thancur
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)
}
def intProxy(x: Int) = new Ord[Int] {
def cmp(that: Int) : Int = x - that
}
scala> import ProxyDemo._
scala> findMin(1000, List(20, 4, 1, 6))(intProxy)
res1: Int = 1
def stringProxy(x: String) = new Ord[String] {
def cmp(that: String) : Int = x compare that
}
scala> import ProxyDemo._
scala> findMin("z", List("apple", "chorizo", "machaca", "adobado"))(stringProxy)
res1: String = adobado
- Add functionality to existing types by letting us view
Intas implementation ofOrd[Int]Stringas implementations ofOrd[String]
- Proxies require us to
- Pass around the relevant proxy functions ...
- Clutter code with ugly conversions ...
def sayHello(implicit n: String) = println("Hello " + n)
def sayHello(implicit n: String) = println("Hello " + n)
implicit val defaultName = "Ranjit"
def sayHello(implicit n: String) = println("Hello " + n)
implicit val defaultName = "Ranjit"
scala> sayHello("Roberto")
Hello Roberto
def sayHello(implicit n: String) = println("Hello " + n)
implicit val defaultName = "Ranjit"
scala> sayHello("Roberto")
Hello Roberto
scala> sayHello
Hello Ranjit
Scala uses types to fill in the blanks
- When missing a value of type
T
- Scala automatically substitutes implicit
Tvalue ...
- ... if such a value is in scope.
Suppose I wrote a function expecting String inputs
scala> def yu(msg: String) = println("Y U NO " + msg)
yu: (msg: String)Unit
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
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)
^
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
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
yu(10)automatically fixed toyu(i2s(10))
- Using the type mismatch and type of
i2s
Proxies require we pass around functions & clutter code...
... but implicits automatically insert proxies where needed!
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)
}
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) */
}
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)
}
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
}
After steps
Make proxy parameters implicit
Remove explicit uses of proxy (Scala automatically inserts them!)
Make proxy functions implicit
After steps
Make proxy parameters implicit
Remove explicit uses of proxy (Scala automatically inserts them!)
Make proxy functions implicit
We get:
scala> val res = findMin(1000, List(10, 2, 30, 14)) // look ma, no proxies!
res: Int = 2
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
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
A <: Ord[A]
Means: For any A with an implicit proxy mapping A to Ord[A]
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.
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
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)
^
- Eh? Oh! We forgot to put the implicit proxies in scope...
First, lets import the proxies into scope ...
scala> import OrdInstances._
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
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
- Exercise: What will
b1 contains "hippo"return now?
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 }
}
}
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 }
}
}
If you have an (implicit) proxies for type A and type B
Then you have an (implicit) proxy for type (A, B)
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))
- Note: last case is truly bootstrapped -- we get nested tuples for free!
- Exercise: Add proxy generators for
List,Set, andMap
Parametric Polymorphism ("type variables", ML style)
def add[A](x: A): Bag[A]
Subtyping + Parametric Polymorphism
def add[A <: Ord[A]](x: A): Bag[A]
Implicitly retrofitting behavior to existing types
def add[A <% Ord[A]](x: A): Bag[A]
- Types enable reuse without compromising safety