Differences

This shows you the differences between two versions of the page.

Link to this comparison view

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>​
 +