F# and Monopoly Simulation Redux
December 17, 2013 2 Comments
Now that I am 4 months into my F# adventure, I thought I would revisit the monopoly simulation that I wrote in August. There are some pretty big differences
1) I am not using the ‘if…then’ construct at all –> rather I am using pattern matching. For example, consider the original communityChest function:
- let communityChest x y =
- if y = 1 then
- 0
- else if y = 2 then
- 10
- else
- x
and the most recent one:
- let communityChest (tile, randomNumber) =
- match randomNumber with
- | 1 -> 0
- | 2 -> 10
- | _ -> tile
“Big deal”, you are saying to yourself (or at least I did). But the power of pattern matching is put on display with the revised chance. The code is much more readable and understandable.
Original:
- let chance x y =
- if y = 1 then
- 0
- else if y = 2 then
- 10
- else if y = 3 then
- 11
- else if y = 4 then
- 39
- else if y = 5 then
- x – 3
- else if y = 6 then
- 5
- else if y = 7 then
- 24
- else if y = 8 then
- if x < 5 then
- 5
- else if x < 15 then
- 15
- else if x < 25 then
- 25
- else if x < 35 then
- 35
- else
- 5
- else if y = 9 then
- if x < 12 then
- 12
- else if x < 28 then
- 28
- else
- 12
- else
- x
Revised:
- let goToNearestRailroad tile =
- match tile with
- | 36|2 -> 5
- | 7 -> 15
- | 17|22 -> 25
- | 33 -> 35
- | _ -> failwith "not on chance"
- let goToNearestUtility tile =
- match tile with
- | 36|2|7 -> 12
- | 12|22|33-> 28
- | _ -> failwith "not on chance"
- let chance (tile, randomNumber) =
- match randomNumber with
- | 1 -> 0
- | 2 -> 10
- | 3 -> 11
- | 4 -> 39
- | 5 -> tile – 3
- | 6 -> 5
- | 7 -> 24
- | 8 -> goToNearestRailroad tile
- | 9 -> goToNearestUtility tile
- | _ -> tile
As a side note, I ditched the x and y values because they are unreadable. When I went back to the code after 3 months, I spent way too long trying to figure out what the heck ‘x’ was. I know that scientific code uses cryptic values, but clean code does not. I changed them and the code became much better.
I then took a look at the move() function. The original:
- let move x y z =
- if x + y > 39 then
- x + y – 40
- else if x + y = 30 then
- 10
- else if x + y = 2 then
- communityChest 2 z
- else if x + y = 7 then
- chance 7 z
- else if x + y = 17 then
- communityChest 17 z
- else if x + y = 22 then
- chance 22 z
- else if x + y = 33 then
- communityChest 33 z
- else if x + y = 36 then
- chance 36 z
- else
- x + y
and the revised:
- let getBoardMove (currentTile, dieTotal) =
- let initialTile = currentTile + dieTotal
- matchinitialTile with
- | 2 ->communityChest (2, random.Next())
- | 7 ->chance (7, random.Next())
- | 17 ->communityChest (17, random.Next())
- | 22 ->chance (22, random.Next())
- | 30 -> 10
- | 33 ->communityChest (2, random.Next())
- | 36 ->chance (7, random.Next())
- | 40|41|42|43|44|45|46|47|48|49|50|51 -> initialTile – 40
- | _ -> initialTile
I am not happy with line 11 above – but apparently there is not a way in F# to do this ‘>40’ or even ‘[40 .. 51]’ in the left hand side of the pattern match.
So far, the biggest changes were to make the values more understandable and to get rid of the if…then statements and replace them with pattern matching. Both these techniques make the code more readable and understandable. The next big change came with the actual game play itself. The original version:
- let simulation =
- let mutable startingTile = 0
- let mutable endingTile = 0
- let mutable doublesCount = 0
- let mutable inJail = false
- let mutable jailRolls = 0
- for diceRoll in 1 .. 10000 do
- let dieOneValue = random.Next(1,7)
- let dieTwoValue = random.Next(1,7)
- let cardDraw = random.Next(1,17)
- let numberOfMoves = dieOneValue + dieTwoValue
- if dieOneValue = dieTwoValue then
- doublesCount <- doublesCount + 1
- else
- doublesCount <- 0
- if inJail = true then
- if doublesCount > 1 then
- inJail <- false
- jailRolls <- 0
- endingTile <- move 10 numberOfMoves cardDraw
- else
- if jailRolls = 3 then
- inJail <- false
- jailRolls <- 0
- endingTile <- move 10 numberOfMoves cardDraw
- else
- inJail <- true
- jailRolls <- jailRolls + 1
- else
- if doublesCount = 3 then
- inJail <- true
- endingTile <- 10
- else
- endingTile <- move startingTile numberOfMoves cardDraw
- printfn "die1: %A + die2: %A = %A FROM %A TO %A"
- dieOneValue dieTwoValue numberOfMoves startingTile endingTile
- startingTile <- endingTile
- tiles.[endingTile] <- tiles.[endingTile] + 1
You will notice that the word ‘’mutable” shows up six times. Using the word mutable in F# is a code smell so I refactored it out like so:
- let rec rollDice (currentTile, rollCount, doublesCount, inJail, jailRollCount)=
- let dieOneValue = random.Next(1,7)
- let dieTwoValue = random.Next(1,7)
- let dieTotal = dieOneValue + dieTwoValue
- let newRollCount = rollCount + 1
- let newDoublesCount =
- if dieOneValue = dieTwoValue then doublesCount + 1
- else 0
- let newTile = getTileMove(currentTile,dieTotal,newDoublesCount,inJail,jailRollCount)
- let newInJail =
- if newTile = 10 then true
- else false
- let newJailRollCount =
- if newInJail = inJail then jailRollCount + 1
- else 0
- let targetTuple = scorecard.[newTile]
- let newTuple = (fst targetTuple, snd targetTuple + 1)
- scorecard.[newTile] <- newTuple
- if rollCount < 10000 then
- rollDice (newTile, newRollCount, newDoublesCount, newInJail, newJailRollCount)
- else
- scorecard
No “mutable” (thanks to recursion) and only 1 assignment. I also wanted to get rid of that one ‘<-‘ and Thomas Petrick was kind enough to demonstrate the correct way to do this on stack overflow. Finally, I had to throw in a supporting function to make the decision logic account for rolling doubles that may put you in jail or may get you out of jail depending on prior state (were you in jail when you rolled doubles, were you out of jail when you rolled doubles for the 3rd time, etc…). I spent way too much time monkeying around with a series of nest if…then statements when it hit me that I should be using tuples and pattern matching:
- let getTileMove (currentTile, dieTotal, doublesCount, inJail, jailRollCount) =
- match (inJail,jailRollCount, doublesCount) with
- | (true,3,_) -> getBoardMove(10,dieTotal)
- | (true,_,_) -> 10
- | (false,_,3) -> 10
- | (false,_,_) -> getBoardMove(10,dieTotal)
So here if the real power of F# on display. I can think of hundreds of applications that I have seen in C#/VB.NET that have a high cyclomatic complexity and hidden bugs that have reared their head at the most inopportune time because of complex business logic using a series of case..switch and/or if..then. statements. Even by putting step into its own function only helps partially because the code is still there –> it is just labeled better.
By using tupled pattern matching, all of that complexity goes away and we have a succinct series of statements that actually reflect how the brain thinks about the problem. By using F#, there are fewer lines of code (and therefore fewer unit tests to maintain) and you can write code that better represents how the wetware is approaching the problem.
You can use | i when i >= 40 && i … instead of | 40|41|42|43|44|45|46|47|48|49|50|51 -> …
Pingback: F# Weekly #51, 2013 | Sergey Tihon's Blog