====== 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:
* 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). \\
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.
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: \\
- make the code easier to use
- 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:**
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.**
* Modify ''eval'' function to also take a **store** as an argument, modify the code accordingly
* Add a new **case class** ''Var'' which allows creating variables as expressions (e.g. ''Var("x") + Atom(1)'' should be a valid expression)
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])
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.
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