Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision | |||
| pp:2023:scala:l05 [2023/03/25 18:41] andrei.cirpici | pp:2023:scala:l05 [2023/04/05 23:03] (current) mihai.udubasa move from old (bad) link | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Lab 4. Data types in Scala ====== | + | ====== Lab 5. Polymorphism ====== | 
| - | Objectives: | + | ===== 5.1. Maps ===== | 
| - | * get familiar with **algebraic data types** | + | Maps are collections of **(key, value)** pairs. Keys should be unique, and every key is associated with a value. \\ | 
| - | * get familiar with **pattern matching** and **recursion** with them | + | Some of the fundamental operations on maps include: | 
| - | + | * retrieving / updating the value associated with a key | |
| - | ==== 4.1 Natural Numbers ==== | + | * adding a **(key, value)** pair | 
| - | Given the following implementation of the natural numbers, solve the next few exercices. | + | * 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). \\ | ||
| + | <note important> maps are **immutable**, functions working with maps return a new updated version instead of modifing the map </note> | ||
| + | Some examples with the most used functions can be found below. | ||
| + | <hidden> | ||
| + | <code scala>let map = Map(1 -> 2, 3 -> 4): Map[Int, Int]</code> | ||
| + | * Adding a (key, value) pair to a map | ||
| <code scala> | <code scala> | ||
| - | trait NaturalNumber | + | map + (5 -> 6) // Map(1 -> 2, 3 -> 4, 5 -> 6) | 
| - | case object Zero extends NaturalNumber | + | map + (3 -> 5) // Map(1 -> 2, 3 -> 5) -- if key exists, it updates the value | 
| - | case class Successor(x: NaturalNumber) extends NaturalNumber | + | |
| </code> | </code> | ||
| - | + | * Removing the pair associated with a key | |
| - | **4.1.1** Write a function which takes two natural numbers, and return their sum. | + | |
| <code scala> | <code scala> | ||
| - | def add(x: NaturalNumber, y: NaturalNumber): NaturalNumber = ??? | + | map - (3 -> 4) // Map(1 -> 2) | 
| </code> | </code> | ||
| - | + | * Querying a value | |
| - | **4.1.2** Write a function which takes two natural numbers, and return their product. | + | |
| <code scala> | <code scala> | ||
| - | def multiply(x: NaturalNumber, y: NaturalNumber): NaturalNumber = ??? | + | 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 | ||
| </code> | </code> | ||
| + | * Higher-order functions | ||
| + | <code scala> | ||
| + | map mapValues (x => x + 5) // Map(1 -> 7, 2 -> 9) | ||
| + | map filterKeys (x => x <= 2) // Map(1 -> 2) | ||
| + | </code> | ||
| + | </hidden> | ||
| - | **4.1.3** Write a function which takes an int and converts it to a NaturalNumber. | + | ==== 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: | ||
| <code scala> | <code scala> | ||
| - | def toNaturalNumber(x: Int): NaturalNumber = ??? | + | case class Gradebook(book: Map[String,Int]) | 
| </code> | </code> | ||
| - | ==== 4.2 Binary Trees ==== | + | **5.1.1.** Add a method ''+'' to the class, which adds a new entry to the gradebook. | 
| - | Given the following implementation of binary trees, solve the next few exercices. | + | |
| <code scala> | <code scala> | ||
| - | trait BTree | + | def + (entry: (String, Int)): Gradebook = ??? | 
| - | case object EmptyTree extends BTree | + | |
| - | case class Node(value: Int, left: BTree, right: BTree) extends BTree | + | |
| </code> | </code> | ||
| - | **4.2.1** Write a function which takes a BinaryTree and returns its depth. | + | **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**. | 
| <code scala> | <code scala> | ||
| - | def depth(tree: BTree): Int = ??? | + | def setGrade(name: String, newGrade: Int): Gradebook = ??? | 
| </code> | </code> | ||
| - | **4.2.2** Write a function which takes a BinaryTree and returns the number of nodes with even number of children. | + | **5.1.3.** Add a method ''++'' which merges two gradebooks, if a student is in both gradebook, take the higher of the two grades. | 
| <code scala> | <code scala> | ||
| - | def evenChildCount(tree: BTree): Int = ??? | + | 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. | ||
| + | ??? | ||
| + | } | ||
| </code> | </code> | ||
| - | **4.2.3** Write a function which takes a BinaryTree and flattens it (turns it into a list containing the values of the nodes). | + | **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). | 
| <code scala> | <code scala> | ||
| - | def flatten(tree: BTree): List[Int] = ??? | + | def gradeNo: Map[Int,Int] = ??? | 
| </code> | </code> | ||
| - | **4.2.4** Write a function which takes a BinaryTree and return the number of nodes whose values follow a ceratain rule. | + | ===== 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: | ||
| <code scala> | <code scala> | ||
| - | def countNodes(tree: BTree, cond: Int => Boolean): Int = ??? | + | trait Expr[A] { | 
| + | def eval(): A | ||
| + | } | ||
| </code> | </code> | ||
| - | **4.2.5** Write a function which takes a BinaryTree and return mirrored BTree. | + | **5.2.1.** Implement case classes ''BoolAtom'', ''BoolAdd'' and ''BoolMult'', which evaluates ''Add'' as boolean //or// and ''Mult'' as boolean //and//. | 
| <code scala> | <code scala> | ||
| - | def mirror(tree: BTree): BTree= ??? | + | 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 = ??? | ||
| + | } | ||
| </code> | </code> | ||
| - | ==== 4.3 Matrix manipulation ==== | + | 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 shall represent matrices as //lists of lists//, i.e. values of type ''[ [Integer ] ]''. Each element in the outer list represents a line of the matrix. | + | 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. | 
| - | Hence, the matrix | + | |
| - | $math[ \displaystyle \left(\begin{array}{ccc} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \\ \end{array}\right)] | + | <code scala> | 
| + | case class Strategy[A] (add: (A,A) => A, mult: (A,A) => A) | ||
| - | will be represented by the list ''[ [1,2,3],[4,5,6],[7,8,9] ]''. | + | trait Expr[A] { | 
| + | def eval(s: Strategy[A]): A | ||
| + | } | ||
| + | </code> | ||
| - | To make signatures more legible, add the //type alias// to your code: | + | **5.2.2.** Adjust the rest of your code with the new definitions (**NOTE**: ''eval'' should have a more general implementation now). Implement ''boolStrategy''. | 
| - | <code scala> type Matrix = List[List[Int]] </code> | + | <code scala> | 
| - | which makes the type-name ''Matrix'' stand for ''[ [Integer] ]''. | + | |
| - | 4.3.1 Write a function that computes the scalar product with an integer: | + | val boolStrategy = Strategy[Boolean]( | 
| + | (left, right) => , // add implementation | ||
| + | (left, right) => // mult implementation | ||
| + | ) | ||
| - | $math[ \displaystyle 2 * \left(\begin{array}{ccc} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \\ \end{array}\right) = \left(\begin{array}{ccc} 2 & 4 & 6 \\ 8 & 10 & 12 \\ 14 & 16 & 18 \\ \end{array}\right)] | + | val expr1 = BoolMult(BoolAdd(BoolAtom(true), BoolAtom(false)), BoolAtom(false)) | 
| + | val expr2 = BoolMult(BoolAdd(BoolAtom(true), BoolAtom(false)), BoolAtom(true)) | ||
| - | <code scala> | + | println(expr1.eval(boolStrategy)) // false | 
| - | def vprod(m: Matrix)(v: Int): Matrix = ??? | + | println(expr2.eval(boolStrategy)) // true | 
| </code> | </code> | ||
| - | 4.3.2 Write a function which adjoins two matrices by extending rows: | + | **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''. | 
| + | <code scala> | ||
| + | case class Atom[A](a: A) extends Expr[A] { | ||
| + | override def eval(f: Strategy[A]) = ??? | ||
| + | } | ||
| - | $math[ \displaystyle \left(\begin{array}{cc} 1 & 2 \\ 3 & 4\\\end{array}\right)  hjoin \left(\begin{array}{cc} 5 & 6 \\ 7 & 8\\\end{array}\right) = \left(\begin{array}{cc} 1 & 2 & 5 & 6 \\ 3 & 4 & 7 & 8\\\end{array}\right) ] | + | 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 = ??? | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | **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:** | ||
| + | <hidden> | ||
| + | In Scala it is possible to define function implementations in traits. Define ''+'' and ''*'' accordingly. | ||
| + | </hidden> | ||
| + | \\ | ||
| + | |||
| + | 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**: | ||
| <code scala> | <code scala> | ||
| - | def join(m1: Matrix, m2: Matrix): Matrix = ??? | + | type Store[A] = Map[String, A] | 
| </code> | </code> | ||
| + | to be a mapping from values of type ''String'' (variable names) to their values. \\ | ||
| - | 4.3.3 Write a function which adjoins two matrices by adding new rows: | + | **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) | ||
| - | $math[ \displaystyle \left(\begin{array}{cc} 1 & 2 \\ 3 & 4\\\end{array}\right)  vjoin \left(\begin{array}{cc} 5 & 6 \\ 7 & 8\\\end{array}\right) = \left(\begin{array}{cc} 1 & 2 \\ 3 & 4 \\ 5 & 6\\ 7 & 8\\ \end{array}\right) ] | + | <hidden> | 
| + | 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. | ||
| + | <code scala> | ||
| + | def eval(s: Strategy[A]) = eval(s, Map[String, A]()) | ||
| + | </code> | ||
| + | </hidden> | ||
| + | \\ | ||
| + | **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: | ||
| <code scala> | <code scala> | ||
| - | def vjoin(m1: Matrix, m2: Matrix): Matrix = ??? | + | def eval (s: Strategy[A], store: Store[A]): Option[A] | 
| </code> | </code> | ||
| + | Modify your implementation accordingly. If an expression uses variables not present in the store, ''eval'' should return ''None''. | ||
| - | 4.3.4 Write a function which adds two matrices. | + | ===== 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. | ||
| + | <code scala> | ||
| + | Map(2 -> 1, 1 -> 2, 0 -> 1) // encodes x^2 + 2*x + 1 | ||
| + | </code> | ||
| + | <code scala> | ||
| + | case class Polynomial (terms: Map[Int,Int]) | ||
| + | </code> | ||
| + | <hidden> | ||
| + | You can override the ''toString'' method to to see your results in a more friendly format: | ||
| + | <code scala> | ||
| + | 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(_ ++ " + " ++ _) | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | </hidden> | ||
| + | \\ | ||
| + | **5.3.1.** Add a method ''*'' which multiplies the polynomial by a given coeficient: | ||
| <code scala> | <code scala> | ||
| - | def msum(m1: Matrix, m2: Matrix): Matrix = ??? | + | def * (n: Int): Polynomial = ??? | 
| </code> | </code> | ||
| - | 4.3.5 Write a function which computes the transposition of a matrix: | + | **5.3.2.** Implement a method ''hasRoot'' which checks if a given integer is a root of the polynomial. | 
| + | <code scala> | ||
| + | def hasRoot(r: Int): Boolean = ??? | ||
| + | </code> | ||
| - | $math[ tr \left(\begin{array}{ccc} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \\ \end{array}\right) = \left(\begin{array}{ccc} 1 & 4 & 7 \\ 2 & 5 & 8 \\ 3 & 6 & 9 \\ \end{array}\right) ] | + | **5.3.3.** Implement a method ''+'' which adds a given polynomial to this one (Hint: this operation is very similar to gradebook merging). | 
| + | <code scala> | ||
| + | def + (p2: Polynomial): Polynomial = ??? | ||
| + | </code> | ||
| + | **5.3.4.** Implement a method ''*'' which multiplies two polynomials. | ||
| <code scala> | <code scala> | ||
| - | def tr(m: Matrix): Matrix = ??? | + | def * (p2: Polynomial): Polynomial = ??? | 
| </code> | </code> | ||
| - | 4.3.6 Write a function which computes the vectorial product of two matrices. | + | **5.3.5.** Implement ''polynomialStrategy'' and try to evaluate a few expressions with polynomials. | 
| - | * (Hint: start by writing a function which computes $math[a_{ij}] for a given line $math[i] and column $math[j] (both represented as lists)) | + | |
| - | * (Hint: write a function which takes a line of matrix m1 and the matrix m2 and computes the respective line from the product) | + | |
| + | <hidden> | ||
| + | You can test your code with the following expression: | ||
| <code scala> | <code scala> | ||
| - | def mprod(m1: Matrix, m2: Matrix): Matrix = ??? | + | 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 | ||
| </code> | </code> | ||
| + | </hidden> | ||
| + | |||