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

Commit 08149a0

Browse files
authored
Implement alternative opaque type alias exercise (#91)
* Checkpoint result of running 'ctma renumber-exercises -f 9 -t 10 -s 1' * Checkpoint result of running 'ctma duplicate-insert-before -n 9' * Rename exercises * Add code for an alternative for the exercise on opaque type aliases - Convert the `ReductionSet` type alias into an opaque version * Duplicate exercise on opaque type aliases for universal equality exrc * Complete exercise on universal equality * Move alternative exercises for opaque type aliases to new offset - Move two exercises and rename one of them
1 parent 61cd340 commit 08149a0

File tree

65 files changed

+2987
-337
lines changed

Some content is hidden

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

65 files changed

+2987
-337
lines changed

exercises/exercise_010_opaque_type_aliases/README.md

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,38 +32,80 @@ type CellUpdates = Vector[(Int, Set[Int])]
3232

3333
To keep things manageable we will only focus on one of these type aliases for
3434
this exercise. Specifically we will convert the last of these type aliases,
35-
`CellUpdates` into an Opaque Type Alias.
35+
`ReductionSet` into an Opaque Type Alias.
3636

37-
- To do that, simply add the keyword `opaque` in front of the type alias
37+
- Start by moving the type alias to a new source file, `ReductionSet`.
38+
39+
- Now add the keyword `opaque` in front of the type alias
3840
declaration and recompile. Do you expect this to compile successfully? If not,
3941
why?
4042

41-
- Use your experience from the exercise on Extension Methods to fix the
42-
compilation errors of the form `value {name} is not a member of
43-
org.lunatechlabs.dotty.sudoku.CellUpdates`
44-
- Tip: Fix all of the compilation errors of this form `value {name} is not a
45-
member of ...` before tackling the other types of error like `Found: ...
46-
Required: ...`
47-
- Tip: Some of the mission members for our new opaque type are generic (i.e.
48-
type-parameterised) methods. Do not be afraid to implement extension
49-
methods that are non-generic (i.e. without type-parameters), which might
50-
mean modifying existing call-sites.
51-
52-
- Now that we have added the necessary extension methods we still have to fix
53-
the remaining errors where we have a value of type `Vector[(Int, Set[Int])]`
54-
but the compiler is expecting the opaque type `CellUpates`
55-
- Tip: This is the point where we have to think how do we _get_ values of
56-
our opaque type
57-
- Tip: In general, an opaque type goes well together with a companion
58-
object.
59-
60-
- Note: Fixing one type of error (e.g. `Found: ... Required: ...`) may reveal
61-
new errors of type `value {name} is not a member of ...`. If that happens, it
62-
is generally easier to first fix the error of type `value {name} is not a
63-
member of ...` by adding an extension method, and then continue with the other
64-
type of error.
43+
- So it seems we need to tackle quite a few compilation errors... Let's get
44+
started.
45+
46+
> Tip: In general, try to fix all of the compilation errors of this form
47+
> `value {name} is not a member of ...` before tackling the other types
48+
> of error like `Found: ... Required: ...`
49+
50+
- The first errors are in the source file `ReductionRules`. As this file contains
51+
two extension methods on `ReductionSet`, moving these to `ReductionSet` will
52+
resolve these errors.
53+
54+
- Next up is an error linked to value `InitialDetailState`. It's an
55+
`InitialDetailState` which is a `ReductionSet`. Again, move it to the
56+
`ReductionSet` source file. You may have to annotate its type.
57+
58+
- Recompile and look at the first few error in source file `SudokuDetailProcessor`.
59+
These are linked to methods `mergeState`, `stateChanges`, and `isFullyReduced`.
60+
As these 3 methods all take a `ReductionSet` as argument, it makes sense to
61+
convert them to extension methods on `ReductionSet`. Do so, and adapt the
62+
call sites to take the change into account. Note that you may have to change
63+
the visibility of the methods.
64+
65+
- Recompile and notice the errors in `SudokuIO`. The relevant code is doing some
66+
kind of pretty print of a `Vector[ReductionSet]` (which is a `Sudoku`; see the
67+
type alias in `TopLevelDefinitions`). Move the method `SudokuRowPrinter` to
68+
`ReductionSet` and convert it to an extension method (name it `printSudokuRow`).
69+
Move the private `sudokuCellRepresentation` helper method along with it. Make
70+
the necessary changes at the call site.
71+
72+
- Recompile. Notice the extension methods on `SudokuField` that are now in error.
73+
Move these to `ReductionSet` and recompile. Methods `randomSwapAround` and
74+
`toRowUpdates` show errors.
75+
76+
- The core issue with method `randomSwapAround` is the mapping on a `ReductionSet`.
77+
One way to tackle this is to add an extension method on `ReductionSet` with the
78+
following signature: `def swapValues(shuffledValuesMap: Map[Int, Int]): ReductionSet`
79+
Make the necessary changes to `randomSwapAround` to make it compile.
80+
81+
- The core issue with method `toRowUpdates` is invoking `zipWithIndex` on `ReductionSet`.
82+
Add a private extension method `zipWithIndex` on `ReductionSet`.
83+
84+
- We've almost eliminated all complilation errors. The last error can be corrected
85+
by adding an apply method in a `ReductionSet` companion object that allows
86+
one to create a `ReductionSet` from a `Vector[CellContent]`.
6587

6688
- Once all the compilation errors are fixed, run the provided tests by executing
6789
the `test` command from the `sbt` prompt and verify that all tests pass
6890

69-
- Verify that the application runs correctly
91+
- Verify that the application runs correctly
92+
93+
## Conclusions
94+
95+
In summary, all we did was changing a single type alias into an opaque one.
96+
97+
It does raise some questions:
98+
99+
- Was it worth the effort?
100+
- What have we gained, if anything?
101+
- Are there alternatives to this approach?
102+
- What are the gotchas
103+
104+
There are many more potential type aliases to which we could apply the same
105+
procedure. One that sticks out is the `Sudoku` type alias
106+
(`type Sudoku = Vector[ReductionSet`).
107+
108+
109+
110+
111+
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package org.lunatechlabs.dotty.sudoku
2+
3+
opaque type ReductionSet = Vector[CellContent]
4+
5+
val InitialDetailState: ReductionSet = cellIndexesVector.map(_ => initialCell)
6+
7+
object ReductionSet:
8+
def apply(vrs: Vector[CellContent]): ReductionSet = vrs
9+
10+
extension (reductionSet: ReductionSet)
11+
12+
def applyReductionRuleOne: ReductionSet =
13+
val inputCellsGrouped = reductionSet.filter {_.size <= 7}.groupBy(identity)
14+
val completeInputCellGroups = inputCellsGrouped filter { (set, setOccurrences) =>
15+
set.size == setOccurrences.length
16+
}
17+
val completeAndIsolatedValueSets = completeInputCellGroups.keys.toList
18+
completeAndIsolatedValueSets.foldLeft(reductionSet) { (cells, caivSet) =>
19+
cells.map { cell =>
20+
if cell != caivSet then cell &~ caivSet else cell
21+
}
22+
}
23+
24+
def applyReductionRuleTwo: ReductionSet =
25+
val valueOccurrences = CELLPossibleValues.map { value =>
26+
cellIndexesVector.zip(reductionSet).foldLeft(Vector.empty[Int]) {
27+
case (acc, (index, cell)) =>
28+
if cell contains value then index +: acc else acc
29+
}
30+
}
31+
32+
val cellIndexesToValues =
33+
CELLPossibleValues
34+
.zip(valueOccurrences)
35+
.groupBy ((value, occurrence) => occurrence )
36+
.filter { case (loc, occ) => loc.length == occ.length && loc.length <= 6 }
37+
38+
val cellIndexListToReducedValue = cellIndexesToValues.map { (index, seq) =>
39+
(index, (seq.map ((value, _) => value )).toSet)
40+
}
41+
42+
val cellIndexToReducedValue = cellIndexListToReducedValue.flatMap { (cellIndexList, reducedValue) =>
43+
cellIndexList.map(cellIndex => cellIndex -> reducedValue)
44+
}
45+
46+
reductionSet.zipWithIndex.foldRight(Vector.empty[CellContent]) {
47+
case ((cellValue, cellIndex), acc) =>
48+
cellIndexToReducedValue.getOrElse(cellIndex, cellValue) +: acc
49+
}
50+
51+
def mergeState(cellUpdates: CellUpdates): ReductionSet =
52+
cellUpdates.foldLeft(reductionSet) {
53+
case (stateTally, (index, updatedCellContent)) =>
54+
stateTally.updated(index, stateTally(index) & updatedCellContent)
55+
}
56+
57+
def stateChanges(updatedState: ReductionSet): CellUpdates =
58+
(reductionSet zip updatedState).zipWithIndex.foldRight(cellUpdatesEmpty) {
59+
case (((previousCellContent, updatedCellContent), index), cellUpdates)
60+
if updatedCellContent != previousCellContent =>
61+
(index, updatedCellContent) +: cellUpdates
62+
63+
case (_, cellUpdates) => cellUpdates
64+
}
65+
66+
def isFullyReduced: Boolean =
67+
val allValuesInState = reductionSet.flatten
68+
allValuesInState == allValuesInState.distinct
69+
70+
def swapValues(shuffledValuesMap: Map[Int, Int]): ReductionSet =
71+
reductionSet.map(cell => Set(shuffledValuesMap(cell.head)))
72+
73+
private def zipWithIndex: Vector[(CellContent, Int)] = reductionSet.zipWithIndex
74+
75+
end extension // ReductionSet
76+
77+
private def sudokuCellRepresentation(content: CellContent): String =
78+
content.toList match
79+
case Nil => "x"
80+
case singleValue +: Nil => singleValue.toString
81+
case _ => " "
82+
83+
84+
extension (vrs: Vector[ReductionSet])
85+
def printSudokuRow: String =
86+
val rowSubBlocks = for
87+
row <- vrs
88+
rowSubBlock <- row.map(el => sudokuCellRepresentation(el)).sliding(3, 3)
89+
rPres = rowSubBlock.mkString
90+
91+
yield rPres
92+
rowSubBlocks.sliding(3, 3).map(_.mkString("", "|", "")).mkString("|", "|\n|", "|\n")
93+
94+
// Collective Extensions:
95+
// define extension methods that share the same left-hand parameter type under a single extension instance.
96+
extension (sudokuField: SudokuField)
97+
98+
def mirrorOnMainDiagonal: SudokuField = SudokuField(sudokuField.sudoku.transpose)
99+
100+
def rotateCW: SudokuField = SudokuField(sudokuField.sudoku.reverse.transpose)
101+
102+
def rotateCCW: SudokuField = SudokuField(sudokuField.sudoku.transpose.reverse)
103+
104+
def flipVertically: SudokuField = SudokuField(sudokuField.sudoku.reverse)
105+
106+
def flipHorizontally: SudokuField = sudokuField.rotateCW.flipVertically.rotateCCW
107+
108+
def rowSwap(row1: Int, row2: Int): SudokuField =
109+
SudokuField(
110+
sudokuField.sudoku.zipWithIndex.map {
111+
case (_, `row1`) => sudokuField.sudoku(row2)
112+
case (_, `row2`) => sudokuField.sudoku(row1)
113+
case (row, _) => row
114+
}
115+
)
116+
117+
def columnSwap(col1: Int, col2: Int): SudokuField =
118+
sudokuField.rotateCW.rowSwap(col1, col2).rotateCCW
119+
120+
def randomSwapAround: SudokuField =
121+
import scala.language.implicitConversions
122+
val possibleCellValues = Vector(1,2,3,4,5,6,7,8,9)
123+
// Generate a random swapping of cell values. A value 0 is used as a marker for a cell
124+
// with an unknown value (i.e. it can still hold all values 0 through 9). As such
125+
// a cell with value 0 should remain 0 which is why we add an entry to the generated
126+
// Map to that effect
127+
val shuffledValuesMap =
128+
possibleCellValues.zip(scala.util.Random.shuffle(possibleCellValues)).to(Map) + (0 -> 0)
129+
SudokuField(sudokuField.sudoku.map { row =>
130+
row.swapValues(shuffledValuesMap)
131+
})
132+
133+
def toRowUpdates: Vector[SudokuDetailProcessor.RowUpdate] =
134+
sudokuField
135+
.sudoku
136+
.map(_.zipWithIndex)
137+
.map(row => row.filterNot(_._1 == Set(0)))
138+
.zipWithIndex.filter(_._1.nonEmpty)
139+
.map { (c, i) =>
140+
SudokuDetailProcessor.RowUpdate(i, c.map(_.swap))
141+
}

exercises/exercise_010_opaque_type_aliases/src/main/scala/org/lunatechlabs/dotty/sudoku/SudokuDetailProcessor.scala

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class SudokuDetailProcessor[DetailType <: SudokoDetailType : UpdateSender] priva
5555
Behaviors.receiveMessage {
5656
case Update(cellUpdates, replyTo) if ! fullyReduced =>
5757
val previousState = state
58-
val updatedState = mergeState(state, cellUpdates)
58+
val updatedState = state.mergeState(cellUpdates)
5959
if updatedState == previousState && cellUpdates != cellUpdatesEmpty then
6060
replyTo ! SudokuDetailUnchanged
6161
Behaviors.same
@@ -69,8 +69,8 @@ class SudokuDetailProcessor[DetailType <: SudokoDetailType : UpdateSender] priva
6969
// The following can also be written as:
7070
// given ActorRef[Response] = replyTo
7171
// updateSender.sendUpdate(id, stateChanges(state, transformedUpdatedState))
72-
updateSender.sendUpdate(id, stateChanges(state, transformedUpdatedState))(using replyTo)
73-
operational(id, transformedUpdatedState, isFullyReduced(transformedUpdatedState))
72+
updateSender.sendUpdate(id, state.stateChanges(transformedUpdatedState))(using replyTo)
73+
operational(id, transformedUpdatedState, transformedUpdatedState.isFullyReduced)
7474

7575
case Update(cellUpdates, replyTo) =>
7676
replyTo ! SudokuDetailUnchanged
@@ -85,22 +85,4 @@ class SudokuDetailProcessor[DetailType <: SudokoDetailType : UpdateSender] priva
8585

8686
}
8787

88-
private def mergeState(state: ReductionSet, cellUpdates: CellUpdates): ReductionSet =
89-
cellUpdates.foldLeft(state) {
90-
case (stateTally, (index, updatedCellContent)) =>
91-
stateTally.updated(index, stateTally(index) & updatedCellContent)
92-
}
93-
94-
private def stateChanges(state: ReductionSet, updatedState: ReductionSet): CellUpdates =
95-
(state zip updatedState).zipWithIndex.foldRight(cellUpdatesEmpty) {
96-
case (((previousCellContent, updatedCellContent), index), cellUpdates)
97-
if updatedCellContent != previousCellContent =>
98-
(index, updatedCellContent) +: cellUpdates
99-
100-
case (_, cellUpdates) => cellUpdates
101-
}
102-
103-
private def isFullyReduced(state: ReductionSet): Boolean =
104-
val allValuesInState = state.flatten
105-
allValuesInState == allValuesInState.distinct
10688

exercises/exercise_010_opaque_type_aliases/src/main/scala/org/lunatechlabs/dotty/sudoku/SudokuIO.scala

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,10 @@ import java.util.NoSuchElementException
44

55
object SudokuIO:
66

7-
private def sudokuCellRepresentation(content: CellContent): String =
8-
content.toList match
9-
case Nil => "x"
10-
case singleValue +: Nil => singleValue.toString
11-
case _ => " "
12-
13-
private def sudokuRowPrinter(threeRows: Vector[ReductionSet]): String =
14-
val rowSubBlocks = for
15-
row <- threeRows
16-
rowSubBlock <- row.map(el => sudokuCellRepresentation(el)).sliding(3,3)
17-
rPres = rowSubBlock.mkString
18-
19-
yield rPres
20-
rowSubBlocks.sliding(3,3).map(_.mkString("", "|", "")).mkString("|", "|\n|", "|\n")
21-
227
def sudokuPrinter(result: SudokuSolver.SudokuSolution): String =
238
result.sudoku
249
.sliding(3,3)
25-
.map(sudokuRowPrinter)
10+
.map(_.printSudokuRow)
2611
.mkString("\n+---+---+---+\n", "+---+---+---+\n", "+---+---+---+")
2712

2813
/*

exercises/exercise_010_opaque_type_aliases/src/main/scala/org/lunatechlabs/dotty/sudoku/SudokuSolver.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ object SudokuSolver:
2020
export Response.SudokuSolution
2121

2222
type CommandAndResponses = Command | SudokuDetailProcessor.Response | SudokuProgressTracker.Response
23-
23+
2424
import SudokuDetailProcessor.UpdateSender
2525

2626
def genDetailProcessors[A <: SudokoDetailType: UpdateSender](
@@ -87,35 +87,35 @@ class SudokuSolver private (context: ActorContext[SudokuSolver.CommandAndRespons
8787
case SudokuDetailProcessor.RowUpdate(rowNr, updates) =>
8888
updates.foreach { (rowCellNr, newCellContent) =>
8989
val (columnNr, columnCellNr) = rowToColumnCoordinates(rowNr, rowCellNr)
90-
val columnUpdate = CellUpdates(columnCellNr -> newCellContent)
90+
val columnUpdate = Vector(columnCellNr -> newCellContent)
9191
columnDetailProcessors(columnNr) ! SudokuDetailProcessor.Update(columnUpdate, context.self)
9292

9393
val (blockNr, blockCellNr) = rowToBlockCoordinates(rowNr, rowCellNr)
94-
val blockUpdate = CellUpdates(blockCellNr -> newCellContent)
94+
val blockUpdate = Vector(blockCellNr -> newCellContent)
9595
blockDetailProcessors(blockNr) ! SudokuDetailProcessor.Update(blockUpdate, context.self)
9696
}
9797
progressTracker ! SudokuProgressTracker.NewUpdatesInFlight(2 * updates.size - 1)
9898
Behaviors.same
9999
case SudokuDetailProcessor.ColumnUpdate(columnNr, updates) =>
100100
updates.foreach { (colCellNr, newCellContent) =>
101101
val (rowNr, rowCellNr) = columnToRowCoordinates(columnNr, colCellNr)
102-
val rowUpdate = CellUpdates(rowCellNr -> newCellContent)
102+
val rowUpdate = Vector(rowCellNr -> newCellContent)
103103
rowDetailProcessors(rowNr) ! SudokuDetailProcessor.Update(rowUpdate, context.self)
104104

105105
val (blockNr, blockCellNr) = columnToBlockCoordinates(columnNr, colCellNr)
106-
val blockUpdate = CellUpdates(blockCellNr -> newCellContent)
106+
val blockUpdate = Vector(blockCellNr -> newCellContent)
107107
blockDetailProcessors(blockNr) ! SudokuDetailProcessor.Update(blockUpdate, context.self)
108108
}
109109
progressTracker ! SudokuProgressTracker.NewUpdatesInFlight(2 * updates.size - 1)
110110
Behaviors.same
111111
case SudokuDetailProcessor.BlockUpdate(blockNr, updates) =>
112112
updates.foreach { (blockCellNr, newCellContent) =>
113113
val (rowNr, rowCellNr) = blockToRowCoordinates(blockNr, blockCellNr)
114-
val rowUpdate = CellUpdates(rowCellNr -> newCellContent)
114+
val rowUpdate = Vector(rowCellNr -> newCellContent)
115115
rowDetailProcessors(rowNr) ! SudokuDetailProcessor.Update(rowUpdate, context.self)
116116

117117
val (columnNr, columnCellNr) = blockToColumnCoordinates(blockNr, blockCellNr)
118-
val columnUpdate = CellUpdates(columnCellNr -> newCellContent)
118+
val columnUpdate = Vector(columnCellNr -> newCellContent)
119119
columnDetailProcessors(columnNr) ! SudokuDetailProcessor.Update(columnUpdate, context.self)
120120
}
121121
progressTracker ! SudokuProgressTracker.NewUpdatesInFlight(2 * updates.size - 1)

0 commit comments

Comments
 (0)