Programmation appliquée en Scala

Copyright © Cay S. Horstmann 2015 Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License

Variance

Given classes

class Pair[T](val first: T, val second: T)
class Person(val name: String)
class Student(name: String, val major: String) extends Person(name)
and a method
def makeFriends(p: Pair[Person]): Unit = { ... }
which of the following can be passed to makeFriends?

  1. Only a Pair[Person]
  2. A Pair[Person] and a Pair[Student]
  3. A Pair[Person] and a Pair[Any]
  4. Any Pair[...]

Covariance

Contravariance

A Friend[T] is willing to befriend anyone of type T.

trait Friend[T] {
def befriend(someone: T)
}

Suppose you have a function

def makeFriendWith(s: Student, f: Friend[Student]): Unit = { f.befriend(s) }

What should you be able to pass in for f?

  1. Only a Friend[Student]
  2. A Friend[Person] and a Friend[Student]
  3. A Friend[GraduateStudent] and a Friend[Student]
  4. Any Friend[...]

Contravariance

  1. A generic type G is contravariant if
    S <: T => G[T] <: G[S]
  2. For example,
    Student <: Person => Friend[Person] <: Friend[Student]
  3. Indicated with - annotation:
    trait Friend[-T] {
      def befriend(someone: T)
    }

Producers and Consumers

Mutable Collections

Co- and Contravariant Positions

Position Workaround

Objects Can't Be Generic

Declaration-Site vs. Use-Site Variance

Lab

Scary looking lab

Part 1: Co- and Contravariant Positions

  1. Look at the count, groupBy, and aggregate methods of the Iterable[+A] trait. Why is it in a covariant position in these methods?
  2. In the “Position Workaround” slide, the replaceFirst method has a type bound. Why can’t you define an equivalent method on a mutable Pair[T]?
    def replaceFirst[R >: T](newFirst: R) { first = newFirst } // Error
  3. It seems unintuitive to disallow the simple version of the replaceFirst method for an immutable Pair[+T]. But there is a reason. Suppose you could define
    def replaceFirst(newFirst: T) = new Pair(newFirst, second)
    
    Then you could also define
    class NastyDoublePair(x: Double, y: Double) extends Pair[Double](x, y) {
      override def replaceFirst(newFirst: Double)
        = new Pair(math.sqrt(newFirst), second)
    }
    Now what would happen with this call?
    val p: Pair[Any] = new NastyDoublePair(3, 4)
    val q = p.replaceFirst("Hello")
    Is the first line valid? That is, can you assign a NastyDoublePair to a Pair[Any]?
  4. Is the second line valid? That is, can you call Pair[Any].replaceFirst with a "String" (assuming that it is defined as above?)
  5. What is the problem?
  6. What does Scala do to avoid the problem?

Part 2: Designing With Variance

  1. Let's use the world's simplest map data structure:
    class MyMap[K, V](entries: List[(K, V)]) {
      def this() { this(Nil) }
      def get(key: K) = entries.find(_._1 == key) match {
      	case Some(e) => Some(e._2)
      	case None => None
      }
      def put(key: K, value: V) = new MyMap((key, value) :: entries)
    }
      
    Make an instance as
    val mm = new MyMap[String, Int].put("Fred", 1).put("Wilma", 10).put("Fred", 5)
    What do you get for
    mm.get("Fred")
    mm.get("Barney")
  2. Now let's say we have a function that needs to look up scores for students:
    def total(students: List[Student], scores: MyMap[Student, Int]) =
      students.map(scores.get(_).getOrElse(0)).sum
    
    Should it be ok to pass it a MyMap[Person, Int]?
  3. Adjust the code for MyMap to make that work. What did you do?
  4. Now let's try this the other way around.
    def findById(ids: List[Int], people: MyMap[Int, Person]) =
      ids.flatMap(people.get(_))
    
    Should it be ok to pass it a MyMap[Int, Student]?
  5. Make the same adjustment. What happens?
  6. Using the position workaround, fix it so that it works. What did you do?
  7. Why isn't scala.collection.immutable.Map[A, +B] contravariant in A?

Part 3: Order in the Court!

  1. Let's order people by name:
    class Person(val name: String) extends Ordered[Person] {
        def compare(that: Person) = name.compareTo(that.name)
    }
    
    class Student(name: String, val major: String) extends Person(name)
    
    Make a Person and a Student object. Can you call person.compare(student)? student.compare(person)?
  2. Make a Vector[Person], fill it with some people, and call people.max. What happens.
  3. Repeat with a Vector[Student]. What happens?
  4. Why can't it work? (Hint: Look carefully at the definition of Ordered in Scaladoc.)
  5. Locally define Ordered to be a generic trait with a single abstract method compare(that: T) : Int. Make it contravariant so that a Ordered[Person] is also a Ordered[Student]. Define max[T <: Ordered[T]](s: Seq[T]) = s.reduce(...). What is max?
  6. What happens when you call max(people)? max(students)?
  7. So why isn't Ordered contravariant? It's been discussed at great length here. Just have a glance at it and admire the complexity and the emotions :-)
  8. So, what to do? First off, remove your definition of Ordered, or make T invariant. The call max(students) should no longer compile. In Java, you'd do this:
    <T extends Comparable<? super T>> max(Collection<T> coll)
    or in Scala notation
    def max[T <: Ordered[_ >: T]]
    Why does this solve the problem?
  9. What happens when you try it?
  10. That's annoying, but there is a workaround. Define
    type SuperOrdered[T] = Ordered[_ >: T]
    def max[T <: SuperOrdered[T]]
    Now what happens?

Homework

Do this as individual work, not with your partner

When all done, email the signed zip files to Fatemeh.Borran@heig-vd.ch