L06. Polymorphism

/*
   (A) Subtype polymorphism:
 
   A type S is related to another type T (supertype) by some notion
   of substitutability, meaning that functions written to operate on elements
   of type T(supertype) can also operate on elements of type S (subtype)
 
   Example:
 */
 
class Animal {
  def scream: String = "An animal screams!"
}
 
class Wolf extends Animal {
  override def scream: String = "A wolf screams"
}
 
new Animal().scream
 
/*
 (B) Ad-hoc polymorphism
 
     Is obtained when a function works or appears to work on
     several different types (which may not have anything in common),
     and that function may behave in unrelated ways, for each type.
     (Strachey 1967)
 
 
 (C) Parametric polymorphism
 
     Functions (or datatypes) which can be defined GENERICALLY, so that
     they work "identically" on families of types, without depending on them.
 
     Examples:
        - computing the size of a list
        - many other operations working on lists, and NOT DEPENDING on
          the contained type of the list.
 
     Scala "generics" (a little different from Java Generics)
 
     We will implement a very simple generic type:
 
 */
trait FList[A] { // a polymorphic trait, it depends on some type A. A is called type variable
  def length: Int
  def head: A
  def tail: FList[A]
  def map[B](f: A => B):FList[B] //B is another type variable
  def foldRight[B](acc: B)(op: (A,B) => B): B
  def foldLeft[B](acc: B)(op: (B,A) => B): B
 
  // we can have implementations in traits
  def contains(e: A): Boolean =
    this.foldRight(false)(_ == e || _)
 
  def indexOf(e: A): Int
  def update(e: A, pos: Int): FList[A]
  // Insertion sort
  // inserting in a sorted list
  def insSorted(f: A => Int)(x: A): FList[A]
  // sorting using insertion sort
  def sortBy(f: A => Int): FList[A]
 
}
 
case class FNil[A]() extends FList[A]{
  override def length: Int = 0
  override def head: A = throw new Exception ("head of empty list")
 
  override def map[B](f: A => B): FList[B] = FNil()
  override def foldRight[B](acc: B)(op :(A,B) => B):B = acc
 
  override def foldLeft[B](acc: B)(op: (B,A) => B): B = acc
}
 
case class Cons[A](x: A, xs: FList[A]) extends FList[A]{
  override def length: Int = 1 + xs.length
  override def head: A = x
 
  override def map[B](f: A => B): FList[B] =
    // awkward silence
    Cons(f(x),xs.map(f))
 
  override def foldRight[B](acc: B)(op :(A,B) => B):B =
    op(x,xs.foldRight(acc)(op))
 
  override def foldLeft[B](acc: B)(op: (B,A) => B): B =
    xs.foldLeft(op(acc,x))(op)
}