Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
pp:2023:scala:l06 [2023/04/02 11:36] tpruteanu |
pp:2023:scala:l06 [2023/04/08 07:49] (current) pdmatei |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Lab 5. Polymorphism ====== | + | ====== Lab 6. Scala on steroids ====== |
- | ===== 5.1. Maps ===== | + | ==== 5-Tic-Tac-Toe ==== |
- | 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). \\ | + | |
- | <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> | + | |
- | 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 | + | |
- | </code> | + | |
- | * Removing the pair associated with a key | + | |
- | <code scala> | + | |
- | map - (3 -> 4) // Map(1 -> 2) | + | |
- | </code> | + | |
- | * Querying a value | + | |
- | <code scala> | + | |
- | 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> | + | |
- | * 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> | + | |
- | ==== Exercises ==== | + | **Tic Tac Toe** is usually played on a 3x3 board, marking positions by each player in rounds. Our game is slightly different (usually called 5-in-a-row): |
- | 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: | + | * it can be played on a square board of any size **larger or equal to 5**. |
- | <code scala> | + | * A player wins if it has marked a line, column or diagonal of **5 consecutive positions** in a row. |
- | case class Gradebook(book: Map[String,Int]) | + | |
- | </code> | + | |
- | **5.1.1.** Add a method ''+'' to the class, which adds a new entry to the gradebook. | + | Example of a winning position for ''X'' on a 5x5 board: |
- | <code scala> | + | <code> |
- | def + (entry: (String, Int)): Gradebook = ??? | + | X...0 |
+ | 0X.0. | ||
+ | ..X0. | ||
+ | ...X. | ||
+ | .0..X | ||
</code> | </code> | ||
- | **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**. | + | Example of a winning position for ''0'' on a 7x7 board: |
- | <code scala> | + | <code> |
- | def setGrade(name: String, newGrade: Int): Gradebook = ??? | + | .X...X. |
+ | ...0... | ||
+ | ...0... | ||
+ | .X.0..X | ||
+ | 0..0..0 | ||
+ | ...0... | ||
+ | ...X... | ||
</code> | </code> | ||
- | **5.1.3.** Add a method ''++'' which merges two gradebooks, if a student is in both gradebook, take the higher of the two grades. | + | ==== Encodings ==== |
- | <code scala> | + | |
- | 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> | + | |
- | **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). | + | * In your project template, ''X'' is encoded as the **first** player (''One''), and ''0'', as ''Two''. |
<code scala> | <code scala> | ||
- | def gradeNo: Map[Int,Int] = ??? | + | trait Player {} |
- | </code> | + | case object One extends Player { |
- | + | override def toString: String = "X" | |
- | ===== 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> | + | |
- | trait Expr[A] { | + | |
- | def eval(): A | + | |
} | } | ||
- | </code> | + | case object Two extends Player { |
- | + | override def toString: String = "0" | |
- | **5.2.1.** Implement case classes ''BoolAtom'', ''BoolAdd'' and ''BoolMult'', which evaluates ''Add'' as boolean //or// and ''Mult'' as boolean //and//. | + | |
- | <code scala> | + | |
- | 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] { | + | case object Empty extends Player { |
- | override def eval(): Boolean = ??? | + | override def toString: String = "." |
- | } | + | |
- | case class BoolMult(left: Expr[Boolean], right: Expr[Boolean]) extends Expr[Boolean] { | + | |
- | override def eval(): Boolean = ??? | + | |
} | } | ||
</code> | </code> | ||
- | + | * A ''Board'' is encoded as a List of Lists of **positions** (i.e. a matrix), where a position can be ''One'', ''Two'' or ''Empty''. We make no distinction in the code between a position and a player, although ''Empty'' cannot be seen as a valid player. This makes the code slightly easier to write. | |
- | 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. | + | |
<code scala> | <code scala> | ||
- | case class Strategy[A] (add: (A,A) => A, mult: (A,A) => A) | + | type Line = List[Player] |
+ | type BoardList = List[Line] | ||
- | trait Expr[A] { | + | case class Board(b: BoardList) { |
- | def eval(s: Strategy[A]): A | + | override def toString: String = ??? |
} | } | ||
</code> | </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''. | + | ==== Tasks ==== |
- | <code scala> | + | The following functions have a given signature. However, it is up to the student to decide whether these will be methods of a class or just simple functions. |
- | val boolStrategy = Strategy[Boolean]( | + | **6.1.1.** Write a function which converts a string into a ''Board''. As a helper, you can use ''_.split( c )'' where c is a separator string, and ''_.toList''. The best solution is to use a combination of ''map'' calls with the above mentioned functions. A string is encoded exactly as in the examples shown above: |
- | (left, right) => , // add implementation | + | * there are no whitespaces - empty positions are marked by the character '.' |
- | (left, right) => // mult implementation | + | * lines are delimited by '\n' (the last line does not have a trailing '\n'). |
- | ) | + | |
- | + | ||
- | 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 | + | |
- | </code> | + | |
- | **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> | <code scala> | ||
- | case class Atom[A](a: A) extends Expr[A] { | + | def makeBoard(s: String): Board = { |
- | override def eval(f: Strategy[A]) = ??? | + | def toPos(c: Char): Player = |
- | } | + | c match { |
- | + | case 'X' => One | |
- | case class Add[A](left: Expr[A], right: Expr[A]) extends Expr[A] { | + | case '0' => Two |
- | override def eval(f: Strategy[A]): A = ??? | + | case _ => Empty |
- | } | + | } |
- | + | ??? | |
- | case class Mult[A](left: Expr[A], right: Expr[A]) extends Expr[A] { | + | |
- | override def eval(f: Strategy[A]): A = ??? | + | |
} | } | ||
</code> | </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! \\ | + | **6.1.2.** Write a function which checks if a position on the board is free. Recall that list indexing can be done using ''l(_)''. Positions are numbered from 0. |
- | \\ | + | |
- | **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> | ||
- | type Store[A] = Map[String, A] | + | def isFree(x:Int, y:Int):Boolean = ??? |
</code> | </code> | ||
- | to be a mapping from values of type ''String'' (variable names) to their values. \\ | ||
- | **5.2.5.** | + | **6.1.3.** Write a function which returns the //opponent// of a player: |
- | * Modify ''eval'' function to also take a **store** as an argument, modify the code accordingly | + | <code scala> |
- | * Add a new **case class** ''Var'' which allows creating variables as expressions (e.g. ''Var("x") + Atom(1)'' should be a valid expression) | + | def complement(p: Player): Player = ??? |
+ | </code> | ||
- | <hidden> | + | **6.1.4.** We want to write a function which converts a board to a string, following the same strategy. Complete the ''toString'' in the Board class. Hint: instead of ''foldRight'', you can use ''reduce'' which works quite similarly, but without requiring an accumulator. |
- | 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: | + | **6.1.5.** Write a function which returns the //columns// of a board: |
<code scala> | <code scala> | ||
- | def eval (s: Strategy[A], store: Store[A]): Option[A] | + | def getColumns: Board = ??? |
</code> | </code> | ||
- | Modify your implementation accordingly. If an expression uses variables not present in the store, ''eval'' should return ''None''. | ||
- | ===== 5.3. Polynomials ===== | + | **6.1.6.** Implement the following two functions for extracting the first and second diagonal, as lines, from a board. Hint: use for comprehensions. |
- | Consider a polynomial encoded as a map, where each present key denotes a power of **x**, and a value denotes its coefficient. | + | |
<code scala> | <code scala> | ||
- | Map(2 -> 1, 1 -> 2, 0 -> 1) // encodes x^2 + 2*x + 1 | + | def getFstDiag(): Line = ??? |
+ | def getSndDiag(): Line = ??? | ||
</code> | </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) | + | **6.1.7.** Implement the following functions for extracting diagonals above/below the first/second diagonal, as lines. It's not really necessary to make sure that at least 5 positions are available, for now. Hint: if one function must be implemented with element-by-element iteration, the three other can be implemented using each-other, as single-line calls. |
- | .map(printRule) | + | |
- | .reduce(_ ++ " + " ++ _) | + | |
- | } | + | |
- | } | + | |
- | </code> | + | |
- | </hidden> | + | |
- | \\ | + | |
- | **5.3.1.** Add a method ''*'' which multiplies the polynomial by a given coeficient: | + | |
<code scala> | <code scala> | ||
- | def * (n: Int): Polynomial = ??? | + | def getAboveFstDiag: List[Line] = ??? |
+ | def getBelowFstDiag: List[Line] = ??? | ||
+ | def getAboveSndDiag: List[Line] = ??? | ||
+ | def getBelowSndDiag: List[Line] = ??? | ||
</code> | </code> | ||
- | **5.3.2.** Implement a method ''hasRoot'' which checks if a given integer is a root of the polynomial. | + | **6.1.8.** Write a function which checks if a player is the winner. Hint: functions ''l.forall(_)'' and ''l.exists(_)'' may be very helpful, together with patterns. |
<code scala> | <code scala> | ||
- | def hasRoot(r: Int): Boolean = ??? | + | def winner(p: Player): Boolean = ??? |
</code> | </code> | ||
- | **5.3.3.** Implement a method ''+'' which adds a given polynomial to this one (Hint: this operation is very similar to gradebook merging). | + | **6.1.9.** Write a function which updates a position from the board, with a given player. The position need not be empty and you are not required to check this. Hint: re-use an inner aux-function together with ''take'' and ''drop''. |
<code scala> | <code scala> | ||
- | def + (p2: Polynomial): Polynomial = ??? | + | def update(p: Player)(ln: Int, col: Int) : Board = ??? |
</code> | </code> | ||
- | **5.3.4.** Implement a method ''*'' which multiplies two polynomials. | + | **6.1.10.** Write a function which generates all possible next-moves for any of the two players. A next-move consists in a new board, where the player-at-hand played his move. |
<code scala> | <code scala> | ||
- | def * (p2: Polynomial): Polynomial = ??? | + | def next(p: Player): List[Board] = ??? |
</code> | </code> | ||
- | **5.3.5.** Implement ''polynomialStrategy'' and try to evaluate a few expressions with polynomials. | + | ==== Testing ==== |
- | <hidden> | + | Use the following board configurations to test your solutions: |
- | You can test your code with the following expression: | + | |
<code scala> | <code scala> | ||
- | val p1 = Polynomial(Map(2 -> 1, 1 -> 2, 0 -> 1)) | + | val t1 = |
- | val p2 = Polynomial(Map(3 -> 1, 1 -> 3, 0 -> 2)) | + | """X0X0X0 |
- | val expr = (Atom(p1) + Atom(p2)) * Atom(p1) | + | |0X0X0X |
+ | |X0X0X0 | ||
+ | |.XX0.. | ||
+ | |X00... | ||
+ | |X0X0X0""".stripMargin | ||
- | println(expr.eval(polynomialStrategy)) | + | val t2 = |
- | // result: x^5 + 3*x^4 + 8*x^3 + 14*x^2 + 11*x + 3 | + | """...... |
+ | |...... | ||
+ | |...... | ||
+ | |.XX... | ||
+ | |.0000. | ||
+ | |......""".stripMargin | ||
+ | |||
+ | val t3 = | ||
+ | """0X0X0. | ||
+ | |000.X0 | ||
+ | |0.0X.. | ||
+ | |0..0.. | ||
+ | |0X..0X | ||
+ | |...X..""".stripMargin | ||
</code> | </code> | ||
- | </hidden> | ||
- | |||
- |