In the first post we built a basic crossword engine in F# using Fable, Feliz, and React. In this post we’ll refactor the code to make it more idiomatic F#.
Data Types
First a recap on the data types which make up the crossword. The grid is made up of a list of list of cells, with the cells being Black or White. Black cells are blocked out spacers, whereas in White cells we must enter a guess.
type White = {
Number: int option
Solution: string
Guess: string
Solved: bool
Id: int // uniquely identify each cell, needed by React
}
type Cell =
| Black
| White of White
type Grid = Cell list list
Looking at the Problem
Function updateGuess
is called every time a value is typed into a cell - it’s connected to the HTML form field’s onChange
event. Given the existing application state
, the cell
which was updated, and the value v
which was typed into the cell it returns a new game state. It doesn’t need to check whether the value was correct. It’s implemented as below:
let updateGuess state cell v =
let newGrid =
state.grid
|> List.map (fun row ->
row
|> List.map (fun c ->
match c with
| Black -> c
| White whiteCell -> if whiteCell.Id = cell.Id then Cell.White {whiteCell with Guess = v} else Cell.White whiteCell
)
)
{ state with grid = newGrid; }
The code is OK, but for such a simple operation it would be nice if it were cleaner. Elsewhere in the code we can see the same pattern repeated. Function checkSolution
is called once the user clicks the Check button. Here we
let checkSolution (state: State): State =
let grid =
state.grid
|> List.map (fun row ->
row |> List.map (fun cell ->
match cell with
| Black -> cell
| White c -> Cell.White (checkWhiteCell c )
)
)
...
When doing any operation with a Grid
we’re really only interested in the White
cells within it. In the code snippets above we iterate through the grid’s rows and columns using List.map
twice, and then have to match on the Cell
type so as only to perform the action on White
cells.
Define map for our Data Types
Let’s define our own gridMap
function. That will allow us to turn the snippets from above into
let updateGuess updatingCell v state =
let newGrid =
state.grid
|> gridMap (fun cell -> if cell.Id = updatingCell.Id then {cell with Guess = v} else cell)
{ state with grid = newGrid }
let checkCellsAndUpdateStateIfSolved (state: State): State =
let grid =
state.grid
|> gridMap (fun c -> { c with Solved = c.Solution = c.Guess })
{ state with grid = grid }
Much cleaner!
The Implementation
We define gridMap
in terms of cellMap
. The operation we provide f
has a function signature of White -> White
. When operating on the grid we’re only interested in White
cells, the Black
cells by definition have nothing in them.
// If we perform an operation on a cell it's always "Do something to a White cell"
let cellMap (f: White -> White) (cell: Cell): Cell =
match cell with
| Black -> Black
| White w -> White (f w)
let gridMap (f: White -> White) (grid: Grid): Grid =
grid |> List.map (List.map (cellMap f))
Conclusion
Next up we’ll look at defining crosswords as JSON files and being able to load them dynamically into the engine.
Full source code is available on Github.