Lets do a kata - Tennis in f# - part1
The tennis kata in f# with refactorings - part1
Whilst at XpManchester a baby steps kata was run by Ian Howarth on the tennis game. As I am wanting to play with something newish (F#) I figured lets have a look at this kata and see what happens. As a result I am very deliberately trying to look forward in ways that were suggested on that night (Eg state machines) but instead want the code to push me by thinking about ideas like connascence.
This is not intended as an intro to F#. My goal here is to develop my coding mechanics in it whilst exploring a kata.
The Goal
The goal of this kata is to score a tennis game. We are not worried about the match (ie sets) just with players scoring points and getting to a win condition.
The format of the results is as follows
score | output |
---|---|
0-0 | “L-L” |
15-0 | “15-L” |
0-30 | “L-30” |
40-40 | “Deuce” |
advA | “Adv PlayerA” |
wonB | “PlayerB Won” |
Hopefully its clear how to get various permutations out!
I decided that the commands in this game would be PlayerAScores
and PlayerBScores
I have decided to use F# because I need practise! I know Elixir well, have used Elm in anger and played with various ML langauges over the years but have never really put the time into F#
I ended up really enjoying it - am definatley going to use it over C# whenever possible. I mainly write code without state in objects these days where possible anyway.
Tools in use
I used Rider after briefly using Visual Studio because Visual Studio wasn’t helping me with refactorings. I don’t think Rider helps that much either (rename works though).
XUnit - I far prefer this framework to NUnit and didnt want to get into any more complex libraries. Simply using F# is innovation enough for me at this point.
Lets get on with it?
What options do we have in order to deliver something useful without doing the whole thing?
This is always the hard part. I decided I wanted to get to a win condition as fast as possible but wanted both players to be able to score.
An alternative could be to get into deuce and advantage first.
Test 1 : L-L
I decided to cover a 0-0 game and make sure that it return “L-L”
[<Fact>]
let ``Score is L-L by default``() =
Assert.Equal("L-L", score initial_state)
This was originally simply Assert.Equal("L-L", "L-L")
I then extracted method and after the next test factored out into a state object.
Test 2 Lets Score a Point : 15-L
[<Fact>]
let ``Score is 15-L when player one scores``() =
let state = initial_state |> handle (PlayerAScores)
Assert.Equal("15-L", score state)
This test required score
to have an implementation, also it introduced a handle
function.
We need some types
The initial state record also got implemented here also. Its design hugely influenced how this kata went.
type PlayerAScore = int
type PlayerBScore = int
type GameState = { playerAScore: PlayerAScore; playerBScore: PlayerBScore }
type Commands =
| PlayerAScores
| PlayerBScores
let initial_state = { playerAScore = 0; playerBScore = 0 }
initial_state
is a record of type GameState
. F# compiler figures out what the type is from the shape of the record.
Digression: Discriminated unions are why we are using F#
Commands
is a discriminated union, By using a discriminated union you can have functions that handle Commands
but can match on the type of command PlayerAScores
or PlayerBScores
and have everything hook up at compile time. You also get nice compiler warnings if you fail to match all cases etc. This is why we are using F# - we get compile time checking on branch statements in a far less heavy way than c#.
In c# we would have to fire up a class for each command - which would implement something like ICommand
and have separate overloaded functions to handle each ICommand
but have no safety wrt knowing if we handled all commands, or when we add a new command where implementations would need to be. This is a huge problem when changing code bases. We like making the compiler do the work.
Back to the point …
At this point I am not generally concerned with design - I am simply trying to get to green as fast as possible. I know the code will tell me how to refactor it.
Lets implement a command handler
let handle command state =
match command with
| PlayerAScores _ -> { state with playerAScore = state.playerAScore + 1 }
| PlayerBScores _ -> { state with playerBScore = state.playerBScore + 1 }
This also uncovered an invariant about winning
If score > 3 and one players has 2 more points than opponent then that player wins
But that realization is jumping the gun, lets get there first!
But how do we get the score out?
Well we can simply match on the numbers and output the right strings …
let score state =
let calculate_score score = match score with
| 0 -> "L"
| 1 -> "15"
| 2 -> "30" // This was added in later tests
| 3 -> "40" // This was added in later tests
| _ -> "unsupported"
(calculate_score state.playerAScore) + "-" + (calculate_score state.playerBScore)
What about the next tests?
Then I wrote some tests to cover other cases that increment the score for for both players
let ``Score is 15-15 when player one scores and player 2 scores``() =
let state = initial_state
|> handle (PlayerAScores)
|> handle (PlayerBScores)
Assert.Equal("15-15", score state)
[<Fact>]
let ``Score is 30-15 when player one scores twice and player 2 scores once``() =
let state = initial_state
|> handle (PlayerAScores)
|> handle (PlayerBScores)
|> handle (PlayerAScores)
Assert.Equal("30-15", score state)
[<Fact>]
let ``Score is 40-15 when player one scores 3 times and player 2 scores once``() =
let state = initial_state
|> handle (PlayerAScores)
|> handle (PlayerBScores)
|> handle (PlayerAScores)
|> handle (PlayerAScores)
Assert.Equal("40-15", score state)
Hey that feels really simple
Yeah so what next?
So What next? Lets test for a player winning
[<Fact>]
let ``Score is PlayerAWins when player one scores 4 times and player 2 scores once``() =
let state = initial_state
|> handle (PlayerAScores)
|> handle (PlayerBScores)
|> handle (PlayerAScores)
|> handle (PlayerAScores)
|> handle (PlayerAScores)
Assert.Equal("Player A Wins", score state)
Well it’s not at all obvious how to make that work. Its different to simply mapping int->string
as we have done so far. We need some logic.
Maybe we can use a transformation pipeline to parse the state
The decisions here were driven by the observation that we know if someone wins simply by comparing scores. If the different in score is > 2 and the scores are > 3 then we have a winner. IE we have already modelled state sufficiently - all we need to do is parse the state correctly.
Often when a new test appears it puts pressure on the design to change. At this point I comment out that test and start refactoring to make the change simple to implement. This is what we are going to do here.
The command handler doesn’t need to change - we have already discovered that we just need to find when the score between the players is > 2. So maybe we can handle this in the score
function - after all, that converts game state into a string
. In a later part of this series I come back to this decision and make the scorer thin with a fatter command handler but for now lets treat this as a state parsing problem.
Elixir has the idea of plugs, which are basically a set of sinks. You pipe some state through a sequence of them and they transform the input part of the state into a result. If we use a discriminated union we can match on whether or not the state has been handled by a state to string parser
. If it has been handled we can just forward that result to the next parser in the chain and finally return the string.
type ScoreState =
| Scored of string
| Unhandled of GameState
So now we have functions of type GameState->ScoreState
but they will in effect return Scored
or Unhandled
. If they recieve a Scored
then they will just forward that to the next handler, if they can handle GameState
then they will return a Scored
otherwise they return Unhandled
. Code probably makes this easier to understand.
So first the function that will take a function to apply and apply it if state has not been scored. We wrap our Unhandled->Scored|Unhandled
functions in this (remember Unhandled
is GameState
)
let apply_if_unscored to_apply state =
match state with
| Scored s -> Scored s
| Unhandled un -> (to_apply un)
Then we need a function to take GameState->String
this sits on the end of the pipeline converting it all to the output.
let score_state_to_string state =
match state with
| Scored s -> s
| Unhandled _ -> "error unhandled scoring scenario"
Now we can wrap our existing score parser in this
let score state =
let apply_if_unscored to_apply state =
match state with
| Scored s -> Scored s
| Unhandled un -> (to_apply un)
let score_state_to_string state =
match state with
| Scored s -> s
| Unhandled _ -> "error unhandled scoring scenario"
let early_game_scorer state =
let calculate_score score = match score with
| 0 -> "L"
| 1 -> "15"
| 2 -> "30"
| 3 -> "40"
| _ -> "unsupported"
let build_score_string state =
Scored((calculate_score state.playerAScore) + "-" + (calculate_score state.playerBScore))
state
|> apply_if_unscored (build_score_string)
(Unhandled state)
|> early_game_scorer
|> score_state_to_string
Lets look at the winner parser
We just did a lot of refactoring, the function for winning should be easy to drop in place now. Lets see what that looks like
let early_game_win_scorer state =
let simple_win_condition (current_player_score: int, other_player_score: int) =
other_player_score < 3 && current_player_score > 3
let handlePlayerAWon (state) =
match simple_win_condition (state.playerAScore, state.playerBScore) with
| true -> Scored "Player A Won"
| false -> Unhandled state
let handlePlayerBWon (state) =
match simple_win_condition (state.playerBScore, state.playerAScore) with
| true -> Scored "Player B Won"
| false -> Unhandled state
state
|> apply_if_unscored (handlePlayerAWon)
|> apply_if_unscored (handlePlayerBWon)
and we hook that into the pipeline as follows
(Unhandled state)
|> early_game_win_scorer
|> early_game_scorer
|> score_state_to_string
So the inserting into the pipeline part was a success, it was just one more thing. It looks almost as simple as the early game scorer - but this apply_if_unscored
is already feeling less than awesome.
We have a few targets for refactoring - handlePlayerAWon
and handlePlayerBWon
look identical for instance. But lets wait until its really obvious what direction to refactor in.
We have a win condition that will work for both players
Things are progressing, we have hit an inflection point, we have built a flexible plugable pipeline for adding new behaviours in a priority.
Adding to this feels like it should be easy. Next we have to look at Deuce and the while advantage part of the game. Hopefully this refactor will facilitate these changes without too much work also
Head over to Part 2 to find out!