Table of Contents

Lab 5. Polymorphism

5.1. Maps

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:

You can find more information on maps on the Scala Docs (https://docs.scala-lang.org/overviews/collections/maps.html).

maps are immutable, functions working with maps return a new updated version instead of modifing the map

Some examples with the most used functions can be found below.

Click to display ⇲

Click to hide ⇱

let map = Map(1 -> 2, 3 -> 4): Map[Int, Int]
  • Adding a (key, value) pair to a map
map + (5 -> 6)  // Map(1 -> 2, 3 -> 4, 5 -> 6)
map + (3 -> 5)  // Map(1 -> 2, 3 -> 5)  -- if key exists, it updates the value
  • Removing the pair associated with a key
map - (3 -> 4)  // Map(1 -> 2)
  • Querying a value
map get 1  // return 2
map get 3  // return 4
map getOrElse (1, 0)  // return 2
map getOrElse (5, 0)  // return 0 (if key doesn't exist, return provided value)
map contains 1  // True
map contains 5  // False
  • Higher-order functions
map mapValues (x => x + 5)  // Map(1 -> 7, 2 -> 9)
map filterKeys (x => x <= 2)  // Map(1 -> 2)

Exercises

We represent a gradebook as a map which holds, for each student (encoded as String), its grade (an Int). We implement a gradebook as a case class, as follows:

case class Gradebook(book: Map[String,Int])

5.1.1. Add a method + to the class, which adds a new entry to the gradebook.

def + (entry: (String, Int)): Gradebook = ???

5.1.2. Add a method setGrade which modifies the grade of a given student. Note that your method should return an updated gradebook, not modify the current one, since the latter is immutable.

def setGrade(name: String, newGrade: Int): Gradebook = ???

5.1.3. Add a method ++ which merges two gradebooks, if a student is in both gradebook, take the higher of the two grades.

def ++(other: Gradebook): Gradebook = {
  // the best strategy is to first implement the update of an entry into an existing Map...
  def updateBook(book: Map[String,Int], pair: (String,Int)): Map[String,Int] = ???
  // and then use a fold to perform updates for all pairs of the current map.
  ???
}

5.1.4. (!) Add a method which returns a map, containing as key, each possible grade (from 1 to 10) and a value, the number of students having that grade. (Hint: follow the same strategy as for the previous exercise).

def gradeNo: Map[Int,Int] = ???

5.2. Polymorphic expressions

In this section we will implement a very simple expression evaluation (which might be part of a programming language), and we will experiment with different features that will:

  1. make the code easier to use
  2. make the code more general, suitable for a broad range of scenarios (easier to extend)

NOTE: In this process, we will have to rewrite and delete parts of the code from previous steps in order to improve it.

Start with the following polymorphic type definition for a expression:

trait Expr[A] {
  def eval(): A
}

5.2.1. Implement case classes BoolAtom, BoolAdd and BoolMult, which evaluates Add as boolean or and Mult as boolean and.

case class BoolAtom(b: Boolean) extends Expr[Boolean] {
  override def eval(): Boolean = b
}
case class BoolAdd(left: Expr[Boolean], right: Expr[Boolean]) extends Expr[Boolean] {
  override def eval(): Boolean = ???
}
case class BoolMult(left: Expr[Boolean], right: Expr[Boolean]) extends Expr[Boolean] {
  override def eval(): Boolean = ???
}

The code structure can be easily deployable for other types, such as Int, Double etc, but it's inconvenient to be defining different types for each such case. In order to make our code more extensible in this sense, we can add a few ingredients.
We will add a new case class Strategy, which stores the operations that can be performed, in our case Add and Mult. And we will have our eval function take a Strategy as a argument.

case class Strategy[A] (add: (A,A) => A, mult: (A,A) => A)
 
trait Expr[A] {
   def eval(s: Strategy[A]): A
}

5.2.2. Adjust the rest of your code with the new definitions (NOTE: eval should have a more general implementation now). Implement boolStrategy.

val boolStrategy = Strategy[Boolean](
    (left, right) => ,  // add implementation
    (left, right) =>    // mult implementation
)
 
val expr1 = BoolMult(BoolAdd(BoolAtom(true), BoolAtom(false)), BoolAtom(false))
val expr2 = BoolMult(BoolAdd(BoolAtom(true), BoolAtom(false)), BoolAtom(true))
 
println(expr1.eval(boolStrategy))  // false
println(expr2.eval(boolStrategy))  // true

5.2.3. If you implemented eval correctly at 6.3.2., you might notice that it doesn't rely on the Boolean type anymore. Add more general case classes Atom, Add and Mult.

case class Atom[A](a: A) extends Expr[A] {
    override def eval(f: Strategy[A]) = ???
}
 
case class Add[A](left: Expr[A], right: Expr[A]) extends Expr[A] {
    override def eval(f: Strategy[A]): A = ???
}
 
case class Mult[A](left: Expr[A], right: Expr[A]) extends Expr[A] {
    override def eval(f: Strategy[A]): A = ???
}

5.2.4. Currently, writing down expressions is cumbersome, due to the escalating number of parentheses. It would be advisable to allow building expressions using a friendlier syntax, such as: Atom(1) + Atom(2) * Atom(3). This is possible if we define the operations + and * as member functions. Then, for instance Atom(1) + Atom(2) could be translate from Atom(1).+(Atom(2)). Where would it be most convenient for you - the programmer, to define functions + and *? Define them!

Hint:

Click to display ⇲

Click to hide ⇱

In Scala it is possible to define function implementations in traits. Define + and * accordingly.


So far, our expressions contain just values (of different sorts). Making a step towards a programming language (like we mentioned above), we would like to also introduce variables in our expressions. To do so, we need to interpret each variable as boolean or int etc. We define a store:

type Store[A] = Map[String, A]

to be a mapping from values of type String (variable names) to their values.

5.2.5.

Click to display ⇲

Click to hide ⇱

You don't have to modify the expressions from 6.3.5, you can use eval with just 1 parameter by overloading the method and calling the new eval with the empty Map.

def eval(s: Strategy[A]) = eval(s, Map[String, A]())


5.2.6. We have not treated the case when the expression uses variables not found in the store. To do so, we change the type signature of eval to:

def eval (s: Strategy[A], store: Store[A]): Option[A]

Modify your implementation accordingly. If an expression uses variables not present in the store, eval should return None.

5.3. Polynomials

Consider a polynomial encoded as a map, where each present key denotes a power of x, and a value denotes its coefficient.

Map(2 -> 1, 1 -> 2, 0 -> 1)  // encodes x^2 + 2*x + 1
case class Polynomial (terms: Map[Int,Int]) 

Click to display ⇲

Click to hide ⇱

You can override the toString method to to see your results in a more friendly format:

case class Polynomial (terms: Map[Int,Int]) {
    override def toString: String = {
        def printRule(x: (Int, Int)): String = x match {
            case (0, coeff) => coeff.toString
            case (1, coeff) => coeff.toString ++ "*x"
            case (p, coeff) => coeff.toString ++ "*x^" ++ p.toString
        }
 
        terms.toList.sortWith(_._1 >= _._1)
                      .map(printRule)
                      .reduce(_ ++ " + " ++ _)
    }
}


5.3.1. Add a method * which multiplies the polynomial by a given coeficient:

def * (n: Int): Polynomial = ???

5.3.2. Implement a method hasRoot which checks if a given integer is a root of the polynomial.

def hasRoot(r: Int): Boolean = ???

5.3.3. Implement a method + which adds a given polynomial to this one (Hint: this operation is very similar to gradebook merging).

def + (p2: Polynomial): Polynomial = ???

5.3.4. Implement a method * which multiplies two polynomials.

def * (p2: Polynomial): Polynomial = ???

5.3.5. Implement polynomialStrategy and try to evaluate a few expressions with polynomials.

Click to display ⇲

Click to hide ⇱

You can test your code with the following expression:

val p1 = Polynomial(Map(2 -> 1, 1 -> 2, 0 -> 1))
val p2 = Polynomial(Map(3 -> 1, 1 -> 3, 0 -> 2))
val expr = (Atom(p1) + Atom(p2)) * Atom(p1)
 
println(expr.eval(polynomialStrategy))
// result: x^5 + 3*x^4 + 8*x^3 + 14*x^2 + 11*x + 3