Lab 5. Variance and implicits
5.1. Variance
We will use a couple of animal types represented as classes for the following exercises.
class Animal(name: String) { override def toString: String = name } case class Cat(name: String) extends Animal(name) case class Dog(name: String) extends Animal(name)
Covariance
5.1.1. We introduce a carrier class. Modify the type signature so that it allows for animal subtypes.
class Carrier[T <: Animal](val content: T)
5.1.2. Evaluate the following and give an explanation as to why they work or not.
val catCarrier: Carrier[Cat] = new Carrier[Cat](Cat("Felix")) val animalCarrier: Carrier[Animal] = catCarrier val dogCarrier: Carrier[Dog] = new Carrier[Dog](Cat("Merv"))
Contravariance
We introduce a veterinarian class. Consider why this class should be contravariant in T
.
class Vet[T <: Animal] { def treat(patient: T): String = { "Can treat " + patient.toString } }
5.1.3. Evaluate the following and give an explanation as to why they work or not.
val generalVet: Vet[Animal] = new Vet[Animal] val dogVet: Vet[Dog] = generalVet dogVet.treat(Cat("Bob")) val catVet: Vet[Cat] = new Vet[Dog]
We'd like to be able to compare animals through the help of a Comparator
class.
5.1.4. Implement a comparator class that compares animals by their name. Determine the type parameter and whether it should be covariant/contravariant/invariant.
enum Ord: case LT, EQ, GT class Comparator[???] { def compare(o1: T, o2: T): Ord = ??? }
5.1.5. Determine which animal would come first alphabetically from a list of animals, using the implemented comparator.
def detFirst(lst: List[Animal]): Animal = { val comparator = new Comparator[Animal] ??? }
5.1.6. analyze the following function signature and come up with a function that could be passed, with sub/supertypes that differ from the signature ones. (Hint: What trait does List extend?)
def apply(g: List[Animal] => Animal)(lst: List[Animal]): Animal = { g(lst) }
5.2. Implicits and extensions
In this section we attempt to implement a basic SAT solver for Boolean variables. We begin by creating a structure capable of representing boolean expressions that also contain variables, and then work on the solver itself. For this section, work in a single sheet, as you will go back and make slight adjustments along the way.
5.2.1. Implement the type BoolExpr
, which contains the following constructors:
True
andFalse
Symbol(s)
- takes a String parameter for the nameAnd(e1, e2)
andOr(e1, e2)
- takes 2 boolean expressionsNot(e)
- takes 1 boolean expression
trait BoolExpr // A case class/object for each required expression subtype
5.2.2. Add an implicit method which allows creating BoolExpr values from the basic “true” and “false” Boolean type values.
implicit def boolToExpr(b: Boolean): BoolExpr = ???
If we want to create expressions at this point, we'd be required to write something like this:
And(Not(Or(Symbol("x"), Symbol("y"))), And(True, Symbol("z")))
Which is very cumbersome. We would like to use a more convenient and standard notation and usage of operators.
5.2.3. Implement an extension that allows you to use &&
, ||
and ~
(Unary not) in order to create expressions more easily.
extension (e: BoolExpr) { ??? }
5.2.4. Implement a method in trait BoolExpr, which returns a list of variables in the expression.
trait BoolExpr { def getVars: List[String] } // Override the function definition in each subclass
Moving forward, we want to be able to evaluate our expressions built through our implemented features. The only BoolExpr that is not reducible through basic boolean algebraic properties is the Symbol
type. To simplify a Symbol
into a True
or False
value, we need to give it such a value in a simulated memory.
This simulated memory will be a Map[String, Boolean]
type, taking a name and mapping it's value, to be used later.
Maps are collections of (key, value) pairs. Keys should be unique, and every key is associated with a value.
Some of the fundamental operations on maps include:
- retrieving / updating the value associated with a key
- adding a (key, value) pair
- removing a (key, value) pair
You can find more information on maps on the Scala Docs (https://docs.scala-lang.org/overviews/collections/maps.html).
Some examples with the most used functions can be found below.
5.2.5. Implement another method in trait BoolExpr, that evaluates the expression, with respect to the stored values given.
trait BoolExpr { def getVars: List[String] def eval(store: Map[String,Boolean]): Boolean } // Override the function definition in each subtype
5.2.6. Create class Solver
, which takes a boolean expression and implement it's 2 member methods:
interpretations: List[Store]
- generates all possible memory possibilities for the given expressionsolve: Option[Store]
- iterates over all stores and returns the first one that satisfies the expression
class Solver(formula: BoolExpr){ type Store = Map[String,Boolean] def interpretations: List[Store] = ??? def solve: Option[Store] = ??? }