Skip to content
This repository was archived by the owner on Mar 27, 2025. It is now read-only.

Commit a975b20

Browse files
authored
Add exercise opaque type aliases (#121)
* Add slide for extra exercise on opaque type aliases * Rename exercise on opaque type aliases - Make it apparent in the exercise name that this is an optional exercise * Checkpoint result of running 'ctma renumber-exercises -f 9 -t 10 -s 1' * Checkpoint result of running 'ctma duplicate-insert-before -n 9' * Add exercise and instructions - Extra exercise on opaque type aliases and comparing them to other alternatives such as value classes * Checkpoint result of running 'ctma renumber-exercises -f 13 -t 20 -s 1'
1 parent 258dc3b commit a975b20

File tree

85 files changed

+1350
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+1350
-2
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Exploring Opaque type aliases
2+
3+
## Background
4+
5+
In the chapter on Opaque type aliases, we did a quick review of the various features in
6+
Scala that allow us to abstract over certain types without incurring the overhead of
7+
boxing/unboxing. We saw that some of these features (Value classes for example) fail
8+
to achieve this goal under certain circumstances.
9+
10+
In this exercise, you will look at the aforementioned features and have a look at the
11+
generated byte code to see whether boxing occurs or not.
12+
13+
## Steps
14+
15+
- Clone this repo: [Moving from Scala 2 to Scala 3](https://github.com/lunatech-labs/lunatech-scala2-to-scala3-course)
16+
17+
- In the cloned repository, `cd` into the `code-snippets` folder
18+
- This folder holds a multi-project `sbt` build. Load this project into your IDE (IntelliJ
19+
or VSCode with Metals)
20+
- We will focus on the `opaque-type-aliases` `sbt` project for this exercise
21+
22+
- Start an `sbt` session in the `code-snippets` folder and compile the project
23+
24+
We will now walk through the different features
25+
26+
### Case class wrappers
27+
28+
- Have a look at the source code in the `CaseClasses.scala` file in the `opaquetypelaliases.caseclasses` package
29+
- The Scala compiler has generated class files for this under the `target/scala-3.3.0/classes/opaquetypealiases/caseclasses` folder
30+
- Decompile the different class files using:
31+
- VSCode with Metals: the `cfr` Java decompiler: right-click on a class file. Select `Metals Analyse Source`/`Show decompiled with CFR`
32+
- IntelliJ: right-click on a class file. Select `Show Decompiled Class As Java`
33+
- Look at the decompiled code and figure out whether boxing occurs or not.
34+
35+
### Value-class wrappers
36+
37+
- Repeat the same steps for file `ValueClasses.scala` in the `opaquetypelaliases.valueclasses` package
38+
39+
- Repeat for `ParametricPolymorphism.scala` in the `opaquetypelaliases.parametricpolymorphism` package
40+
41+
- Repeat for `Subtyping.scala` in the `opaquetypelaliases.subtyping` package
42+
43+
### Opaque type aliases
44+
45+
- Repeat the same steps for file `UsingTheAliases.scala` in the `opaquetypelaliases.opaquetypealias`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.lunatechlabs.dotty.sudoku
2+
3+
// Extension instance wraps extension methods for type ReductionSet
4+
extension (reductionSet: ReductionSet)
5+
6+
def applyReductionRuleOne: ReductionSet =
7+
val inputCellsGrouped = reductionSet.filter { _.size <= 7 }.groupBy(identity)
8+
val completeInputCellGroups = inputCellsGrouped.filter { (set, setOccurrences) =>
9+
set.size == setOccurrences.length
10+
}
11+
val completeAndIsolatedValueSets = completeInputCellGroups.keys.toList
12+
completeAndIsolatedValueSets.foldLeft(reductionSet) { (cells, caivSet) =>
13+
cells.map { cell =>
14+
if cell != caivSet then cell &~ caivSet else cell
15+
}
16+
}
17+
18+
def applyReductionRuleTwo: ReductionSet =
19+
val valueOccurrences = CellPossibleValues.map { value =>
20+
cellIndexesVector.zip(reductionSet).foldLeft(Vector.empty[Int]) { case (acc, (index, cell)) =>
21+
if cell contains value then index +: acc else acc
22+
}
23+
}
24+
25+
val cellIndexesToValues =
26+
CellPossibleValues.zip(valueOccurrences).groupBy((value, occurrence) => occurrence).filter { case (loc, occ) =>
27+
loc.length == occ.length && loc.length <= 6
28+
}
29+
30+
val cellIndexListToReducedValue = cellIndexesToValues.map { (index, seq) =>
31+
(index, seq.map((value, _) => value).toSet)
32+
}
33+
34+
val cellIndexToReducedValue = cellIndexListToReducedValue.flatMap { (cellIndexList, reducedValue) =>
35+
cellIndexList.map(cellIndex => cellIndex -> reducedValue)
36+
}
37+
38+
reductionSet.zipWithIndex.foldRight(Vector.empty[CellContent]) { case ((cellValue, cellIndex), acc) =>
39+
cellIndexToReducedValue.getOrElse(cellIndex, cellValue) +: acc
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.lunatechlabs.dotty.sudoku
2+
3+
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
4+
import akka.actor.typed.{ActorRef, Behavior}
5+
6+
object SudokuDetailProcessor:
7+
8+
// My protocol
9+
enum Command:
10+
case ResetSudokuDetailState
11+
case Update(cellUpdates: CellUpdates, replyTo: ActorRef[Response])
12+
case GetSudokuDetailState(replyTo: ActorRef[SudokuProgressTracker.Command])
13+
export Command.*
14+
15+
// My responses
16+
enum Response:
17+
case RowUpdate(id: Int, cellUpdates: CellUpdates)
18+
case ColumnUpdate(id: Int, cellUpdates: CellUpdates)
19+
case BlockUpdate(id: Int, cellUpdates: CellUpdates)
20+
case SudokuDetailUnchanged
21+
export Response.*
22+
23+
def apply[DetailType <: SudokuDetailType](id: Int, state: ReductionSet = InitialDetailState)(using
24+
updateSender: UpdateSender[DetailType]): Behavior[Command] =
25+
Behaviors.setup { context =>
26+
(new SudokuDetailProcessor[DetailType](context)).operational(id, state, fullyReduced = false)
27+
}
28+
29+
trait UpdateSender[A]:
30+
def sendUpdate(id: Int, cellUpdates: CellUpdates)(using sender: ActorRef[Response]): Unit
31+
def processorName(id: Int): String
32+
33+
given UpdateSender[Row] with
34+
override def sendUpdate(id: Int, cellUpdates: CellUpdates)(using sender: ActorRef[Response]): Unit =
35+
sender ! RowUpdate(id, cellUpdates)
36+
def processorName(id: Int): String = s"row-processor-$id"
37+
38+
given UpdateSender[Column] with
39+
override def sendUpdate(id: Int, cellUpdates: CellUpdates)(using sender: ActorRef[Response]): Unit =
40+
sender ! ColumnUpdate(id, cellUpdates)
41+
def processorName(id: Int): String = s"col-processor-$id"
42+
43+
given UpdateSender[Block] with
44+
override def sendUpdate(id: Int, cellUpdates: CellUpdates)(using sender: ActorRef[Response]): Unit =
45+
sender ! BlockUpdate(id, cellUpdates)
46+
def processorName(id: Int): String = s"blk-processor-$id"
47+
48+
class SudokuDetailProcessor[DetailType <: SudokuDetailType: SudokuDetailProcessor.UpdateSender] private (
49+
context: ActorContext[SudokuDetailProcessor.Command]):
50+
51+
import SudokuDetailProcessor.*
52+
53+
def operational(id: Int, state: ReductionSet, fullyReduced: Boolean): Behavior[Command] =
54+
Behaviors.receiveMessage {
55+
case Update(cellUpdates, replyTo) if !fullyReduced =>
56+
val previousState = state
57+
val updatedState = mergeState(state, cellUpdates)
58+
if updatedState == previousState && cellUpdates != cellUpdatesEmpty then
59+
replyTo ! SudokuDetailUnchanged
60+
Behaviors.same
61+
else
62+
val transformedUpdatedState = updatedState.applyReductionRuleOne.applyReductionRuleTwo
63+
if transformedUpdatedState == state then
64+
replyTo ! SudokuDetailUnchanged
65+
Behaviors.same
66+
else
67+
val updateSender = summon[UpdateSender[DetailType]]
68+
updateSender.sendUpdate(id, stateChanges(state, transformedUpdatedState))(using replyTo)
69+
operational(id, transformedUpdatedState, isFullyReduced(transformedUpdatedState))
70+
71+
case Update(_, replyTo) =>
72+
replyTo ! SudokuDetailUnchanged
73+
Behaviors.same
74+
75+
case GetSudokuDetailState(replyTo) =>
76+
replyTo ! SudokuProgressTracker.SudokuDetailState(id, state)
77+
Behaviors.same
78+
79+
case ResetSudokuDetailState =>
80+
operational(id, InitialDetailState, fullyReduced = false)
81+
82+
}
83+
84+
private def mergeState(state: ReductionSet, cellUpdates: CellUpdates): ReductionSet =
85+
cellUpdates.foldLeft(state) { case (stateTally, (index, updatedCellContent)) =>
86+
stateTally.updated(index, stateTally(index) & updatedCellContent)
87+
}
88+
89+
private def stateChanges(state: ReductionSet, updatedState: ReductionSet): CellUpdates =
90+
state.zip(updatedState).zipWithIndex.foldRight(cellUpdatesEmpty) {
91+
case (((previousCellContent, updatedCellContent), index), cellUpdates)
92+
if updatedCellContent != previousCellContent =>
93+
(index, updatedCellContent) +: cellUpdates
94+
95+
case (_, cellUpdates) => cellUpdates
96+
}
97+
98+
private def isFullyReduced(state: ReductionSet): Boolean =
99+
val allValuesInState = state.flatten
100+
allValuesInState == allValuesInState.distinct
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.lunatechlabs.dotty.sudoku
2+
3+
import java.io.{BufferedReader, File, FileReader}
4+
import java.util.NoSuchElementException
5+
6+
object SudokuIO:
7+
8+
private def sudokuCellRepresentation(content: CellContent): String =
9+
content.toList match
10+
case Nil => "x"
11+
case singleValue +: Nil => singleValue.toString
12+
case _ => " "
13+
14+
private def sudokuRowPrinter(threeRows: Vector[ReductionSet]): String =
15+
val rowSubBlocks = for
16+
row <- threeRows
17+
rowSubBlock <- row.map(el => sudokuCellRepresentation(el)).sliding(3, 3)
18+
rPres = rowSubBlock.mkString
19+
yield rPres
20+
rowSubBlocks.sliding(3, 3).map(_.mkString("", "|", "")).mkString("|", "|\n|", "|\n")
21+
22+
def sudokuPrinter(result: SudokuSolver.SudokuSolution): String =
23+
result.sudoku.sliding(3, 3).map(sudokuRowPrinter).mkString("\n+---+---+---+\n", "+---+---+---+\n", "+---+---+---+")
24+
25+
/*
26+
* FileLineTraversable code taken from "Scala in Depth" by Joshua Suereth
27+
*/
28+
29+
class FileLineTraversable(file: File) extends Iterable[String]:
30+
val fr = new FileReader(file)
31+
val input = new BufferedReader(fr)
32+
var cachedLine: Option[String] = None
33+
var finished: Boolean = false
34+
35+
override def iterator: Iterator[String] = new Iterator[String] {
36+
37+
override def hasNext: Boolean = (cachedLine, finished) match
38+
case (Some(_), _) => true
39+
40+
case (None, true) => false
41+
42+
case (None, false) =>
43+
try
44+
val line = input.readLine()
45+
if line == null then
46+
finished = true
47+
input.close()
48+
fr.close()
49+
false
50+
else
51+
cachedLine = Some(line)
52+
true
53+
catch
54+
case e: java.io.IOError =>
55+
throw new IllegalStateException(e.toString)
56+
57+
override def next(): String =
58+
if !hasNext then throw new NoSuchElementException("No more lines in file")
59+
val currentLine = cachedLine.get
60+
cachedLine = None
61+
currentLine
62+
}
63+
override def toString: String =
64+
"{Lines of " + file.getAbsolutePath + "}"
65+
66+
def convertFromCellsToComplete(cellsIn: Vector[(String, Int)]): Vector[(Int, CellUpdates)] =
67+
for
68+
(rowCells, row) <- cellsIn
69+
updates = rowCells.zipWithIndex.foldLeft(cellUpdatesEmpty) {
70+
case (cellUpdates, (c, index)) if c != ' ' =>
71+
(index, Set(c.toString.toInt)) +: cellUpdates
72+
case (cellUpdates, _) => cellUpdates
73+
}
74+
yield (row, updates)
75+
76+
def readSudokuFromFile(sudokuInputFile: java.io.File): Vector[(Int, CellUpdates)] =
77+
val dataLines = new FileLineTraversable(sudokuInputFile).toVector
78+
val cellsIn =
79+
dataLines
80+
.map { inputLine => """\|""".r.replaceAllIn(inputLine, "") } // Remove 3x3 separator character
81+
.filter(_ != "---+---+---") // Remove 3x3 line separator
82+
.map("""^[1-9 ]{9}$""".r.findFirstIn(_)) // Input data should only contain values 1-9 or ' '
83+
.collect { case Some(x) => x }
84+
.zipWithIndex
85+
86+
convertFromCellsToComplete(cellsIn)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.lunatechlabs.dotty.sudoku
2+
3+
private val N = 9
4+
val CellPossibleValues: Vector[Int] = (1 to N).toVector
5+
val cellIndexesVector: Vector[Int] = Vector.range(0, N)
6+
val initialCell: Set[Int] = Set.range(1, 10)
7+
val InitialDetailState = cellIndexesVector.map(_ => initialCell)
8+
9+
type CellContent = Set[Int]
10+
type ReductionSet = Vector[CellContent]
11+
type Sudoku = Vector[ReductionSet]
12+
13+
type CellUpdates = Vector[(Int, Set[Int])]
14+
val cellUpdatesEmpty = Vector.empty[(Int, Set[Int])]
15+
16+
final case class SudokuField(sudoku: Sudoku)
17+
18+
import SudokuDetailProcessor.RowUpdate
19+
20+
extension (update: Vector[SudokuDetailProcessor.RowUpdate])
21+
def toSudokuField: SudokuField =
22+
import scala.language.implicitConversions
23+
val rows =
24+
update
25+
.map { case SudokuDetailProcessor.RowUpdate(id, cellUpdates) => (id, cellUpdates) }
26+
.to(Map)
27+
.withDefaultValue(cellUpdatesEmpty)
28+
val sudoku = for
29+
(row, cellUpdates) <- Vector.range(0, 9).map(row => (row, rows(row)))
30+
x = cellUpdates.to(Map).withDefaultValue(Set(0))
31+
y = Vector.range(0, 9).map(n => x(n))
32+
yield y
33+
SudokuField(sudoku)
34+
35+
// Collective Extensions:
36+
// define extension methods that share the same left-hand parameter type under a single extension instance.
37+
extension (sudokuField: SudokuField)
38+
39+
def mirrorOnMainDiagonal: SudokuField = SudokuField(sudokuField.sudoku.transpose)
40+
41+
def rotateCW: SudokuField = SudokuField(sudokuField.sudoku.reverse.transpose)
42+
43+
def rotateCCW: SudokuField = SudokuField(sudokuField.sudoku.transpose.reverse)
44+
45+
def flipVertically: SudokuField = SudokuField(sudokuField.sudoku.reverse)
46+
47+
def flipHorizontally: SudokuField = sudokuField.rotateCW.flipVertically.rotateCCW
48+
49+
def rowSwap(row1: Int, row2: Int): SudokuField =
50+
SudokuField(sudokuField.sudoku.zipWithIndex.map {
51+
case (_, `row1`) => sudokuField.sudoku(row2)
52+
case (_, `row2`) => sudokuField.sudoku(row1)
53+
case (row, _) => row
54+
})
55+
56+
def columnSwap(col1: Int, col2: Int): SudokuField =
57+
sudokuField.rotateCW.rowSwap(col1, col2).rotateCCW
58+
59+
def randomSwapAround: SudokuField =
60+
import scala.language.implicitConversions
61+
val possibleCellValues = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9)
62+
// Generate a random swapping of cell values. A value 0 is used as a marker for a cell
63+
// with an unknown value (i.e. it can still hold all values 0 through 9). As such
64+
// a cell with value 0 should remain 0 which is why we add an entry to the generated
65+
// Map to that effect
66+
val shuffledValuesMap =
67+
possibleCellValues.zip(scala.util.Random.shuffle(possibleCellValues)).to(Map) + (0 -> 0)
68+
SudokuField(sudokuField.sudoku.map { row =>
69+
row.map(cell => Set(shuffledValuesMap(cell.head)))
70+
})
71+
72+
def toRowUpdates: Vector[SudokuDetailProcessor.RowUpdate] =
73+
sudokuField.sudoku
74+
.map(_.zipWithIndex)
75+
.map(row => row.filterNot(_._1 == Set(0)))
76+
.zipWithIndex
77+
.filter(_._1.nonEmpty)
78+
.map { (c, i) =>
79+
SudokuDetailProcessor.RowUpdate(i, c.map(_.swap))
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.lunatechlabs.dotty.sudoku
2+
3+
trait SudokuTestHelpers:
4+
5+
def stringToReductionSet(stringDef: Vector[String]): ReductionSet =
6+
for {
7+
cellString <- stringDef
8+
} yield cellString.replaceAll(" ", "").map { _.toString.toInt }.toSet
9+
10+
def stringToIndexedUpdate(stringDef: Vector[String]): CellUpdates =
11+
for {
12+
(cellString, index) <- stringDef.zipWithIndex if cellString != ""
13+
} yield (index, cellString.replaceAll(" ", "").map { _.toString.toInt }.toSet)
14+
15+
def applyReductionRules(reductionSet: ReductionSet): ReductionSet =
16+
reductionSet.applyReductionRuleOne.applyReductionRuleTwo

0 commit comments

Comments
 (0)