Replace Conditional With Pattern-matching Refactoring
(Source/Credits: https://dev.to/vaderdan/replace-conditional-with-pattern-matching-refactoring-3o96)
Use Pattern-matching and SwiftUI to build TicTacToe game
title: Replace Conditional With Pattern-matching Refactoring published: true cover_image: https://thepracticaldev.s3.amazonaws.com/i/u8sq9fvehdywgjszaurw.png description: Use Pattern-matching and SwiftUI to build TicTacToe game tags: swift, ios, swiftui
This is one of my favorite refactorings. It helps to group logic, making code easier to read and extend.
I'm writing this blog post to use as a bookmark.
Very often, I see code which looks like the following:
```swift func fizzbuzz(i: Int) -> String {
if i % 3 != 0 {
if i % 5 != 0 {
return "(i)"
} else {
return "Buzz"
}
} else if i % 5 != 0 {
return "Fizz"
} else {
return "FizzBuzz"
}
}
for i in 1...100 { print(fizzbuzz(i)) } ```
It's quite a mental challenge to see all the possible states and predict what will hapen on given input number. Nested if's
makes that part of the process even harder.
We have to find a good way to represent the base-case and make all of the illegal cases impossible.
Pattern matching is technique in which we compare the values (or in our example: tuple values).., in a way that we could deconstruct the complex data structure, and match it's internal values against pattern: SomeComplexDataStructure == (0, 0) ?
.
Notice that we can use _
as wildcard symbol,
case(0, _)
will match all values where i%3=0
and i%5=anyvalue
```swift func fizzbuzz(i: Int) -> String { let someComplexDataStructure = (i % 3, i % 5) // Tuple complex data structure
switch someComplexDataStructure { case (0, 0): "FizzBuzz" case (0, ): "Fizz" case (, 0): "Buzz" case (, ): "(i)" } } ```
Lets up our game on another level,
Let's go crazy and make a Tic Tac Toe game, that stores all of it's internal state with enum's and uses pattern matching to display each player moves on the board, find who is the winner, ...
Disclaimer: I'll use the new SwiftUI syntax in to build the views (there is github download link bellow)
Here there are the building block types of our game:
Player
represents the two opponents in the game (0
and X
)
Filed
represents the squares on the board: can be either empty or marked by one of the players
GameState
represents which player turn is (playing
), which player won the game or neither of the players have won (draw)
```swift enum Player { case cross, circle }
enum Field { case empty, marked(Player) }
enum GameState { case playing(Player), winner(Player), draw } ```
SwiftUI
We are going to use SwiftUI Views
to build the basic ui for our little game.
SwiftUI is great way to quickly prototype interfaces, declaratively compose views and subviews...
```swift import SwiftUI import PlaygroundSupport
struct App: View { var title: String var body: some View { VStack { Text(title).foregroundColor(.gray) Game() } } }
PlaygroundPage.current.liveView = UIHostingController(rootView: App("TicTacToe")) ```
We have App
component that contains the Game
component that contains Board > Board Row > Square
...
SwiftUI @State binding
The most beneficial thing about SwiftUI is the realtime binding between state variable's changes and redrawing/refreshing the component itself.
If we declare variable with @State
modifier, every change that we make will refresh the subviews of our component:
```swift struct Game: View { @State var board: [[Field]] @State var gameState: GameState
mutating func restart() { self.board = .... } mutating func clickSquare(_ x: Int, y: Int) { self.board = updateBoard(board, gameState, id: "(x) (y)") self.gameState = updateGameState(board, gameState) }
var body: some View { Board(state, onRestart, clickSquare) } } ```
Board variable [[Field]]
The board
variable is 2 dimentional matrix with all the board squares placed by x and y axis coordinate: Every square have value of either empty
or marked
by some of the players (X
,O
)
let y = 0, x = 1
board[y][x] = .marked(.cross)
When we click on a square, a function will set the board[y][x] = .marked(.cross)
, then SwiftUI will refresh the views and show x
under the square we clicked (because board
is @State variable )
Let's go back to pattern matching!
But how we know if click on board
in which square exatly to put X
or should we put O
alternatively?
I'm going to remind us the rules of TicTacToe:
We can place mark one of the remaing empty squares on the board
We can draw
O
orX
inside it (depending on who's player turn is)
The updateBoard
function does this calculation and changes the board
variable's field on x, y coordinate.
```swift func updateBoard(_ board: [[Field]], _ gameState: GameState, id: String) -> [[Field]] { return board.enumerated().map { (indy, row) in return row.enumerated().map { (indx, field) in if "(indx) (indy)" != id { return field }
//if the board current field == clicked square field
switch (gamestate, field) {
//if square is alredy marked -> return same field
(.playing(_), .marked(_)) => return field,
//if the square is empty -> modify it -> as marked by X or O player
(.playing(let player), .empty) => return .marked(player),
//otherwise in every other case, like when game is won, no started.. square is empty
(_, _) => return .empty
}
}
} ```
Excelent the pattern matching will handle all of the other default cases for us for free!
Calculate the game winner, or next-turn player
In the next part we will calculate if the game is finished, who is the winner, or who's the next player turn is.
I'll break the updateGameState
function in tree parts:
-
Get the player for the next-turn
-
Flatten the board 2dimensional matrix to 1dimentional array
[[x0y0, x1y0, x2y0] [x0y1, x1y1, x2y1]]
becomes(x0y0, x1y0, x2y0, x0y1, x1y1, x2y1)
-
Find if the board contains 3 marked fields in row (horizontal, vertical, diagonal), and if so - change the
gameState
to.winner
We're placing mark on square on the board (either X
or O
), then we calculate - do we have any series of 3 squares in row, and if we have 3 squares in row gameState = winner
If we didn't finished playing the game (gameState != winner
gameState != draw
we don't have winner but it's not draw either) ,
we will continue playing and gameState = .playing(the other player)
```swift
func updateGameState(_ board: [[Field]], _ state: GameState) -> GameState {
// 1.
func nextPlayer(state: GameState) -> GameStates {
switch state {
case .plaing(.cross): return .plaing(.circle)
case .plaing(.circle): return .plaing(.cross)
default: return state
}
}
// 2. let b = board.flatmap { $0 } let flatBoard = (b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9])
// 3.
switch flatBoard {
//horizontal lines
case (.marked(.X), .marked(.X), .marked(.X), , , , , , ): return .winner(.cross)
case (, , , .marked(.X), .marked(.X), .marked(.X), , , ): return .winner(.cross)
case (, , , , , , .marked(.X), .marked(.X), .marked(.X)): return .winner(.cross)
case (.marked(.O), .marked(.O), .marked(.O), , , , , , ): return .winner(.circle)
case (, , , .marked(.O), .marked(.O), .marked(.O), , , ): return .winner(.circle)
case (, , , , , , .marked(.O), .marked(.O), .marked(.O)): return .winner(.circle)
//vertical lines
case (.marked(.X), , , .marked(.X), , , .marked(.X), , ): return .winner(.cross)
case (, .marked(.X), , , .marked(.X), , , .marked(.X), ): return .winner(.cross)
case (, , .marked(.X), , , .marked(.X), , , .marked(.X)): return .winner(.cross)
case (.marked(.O), , , .marked(.O), , , .marked(.O), , ): return .winner(.circle)
case (, .marked(.O), , , .marked(.O), , , .marked(.O), ): return .winner(.circle)
case (, , .marked(.O), , , .marked(.O), , , .marked(.O)): return .winner(.circle)
// diagonal lines
case (.marked(.X), , , , .marked(.X), , , , .marked(.X)): return .winner(.cross)
case (, , .marked(.X), , .marked(.X), , .marked(.X), , ): return .winner(.cross)
case (.marked(.O), , , , .marked(.O), , , , .marked(.O)): return .winner(.circle)
case (, , .marked(.O), , .marked(.O), , .marked(.O), , ): return .winner(.circle)
// default -> all fields filled -> draw
case (.marked(), .marked(), .marked(), .marked(), .marked(.O), .marked(), .marked(), .marked(), .marked()): return .draw
// still unfilled fields -> switch to other player
default: return nextPlayer(state)
}
}
```
In effect these 2 functions (updateGameState
, updateBoard
) handle the all of the possible business logic cases for the game: under 100-lines of code.
Conclusion
This is simple and natural refactoring.
Use this refactoring when you see data being mapped to other data or behavior.
One thing to take as lesson from here: Using data-driven in stead of event driven (if/else
) architecture is almost always better.
Not every switch
case is the same. Some times you can't easily convert a nested ifs
to switch
, take for example the updateGameState
function -> tuple with 9 or more values is difficult to write and maintain.
Comments section