Skip to content

Instantly share code, notes, and snippets.

@cboudereau
Created May 17, 2023 08:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cboudereau/ff891382a345ca81e8f4c6c836d9837b to your computer and use it in GitHub Desktop.
Save cboudereau/ff891382a345ca81e8f4c6c836d9837b to your computer and use it in GitHub Desktop.
Tennis kata in fsharp with KISS principle + make illegal state unrepresentable
type Point = Love | Fifteen | Thirty | Forty
module Point =
//TIP / Closure of operation : Can the score go backward ?
let next = function Love -> Fifteen | Fifteen -> Thirty | Thirty | Forty -> Forty
type Player = Left | Right
(* TIP / Primitive obsession
- Negative score
- 20, 99, 100 score ? *)
type Score =
| Points of (Point * Point)
| FifteenAll
| ThirtyAll
| Deuce
| Advantage of Player
| Game of Player
module Score =
let start = Points (Love, Love)
// TIP / Make illegal states unrepresentable
module Player =
// TIP / Concrete type transformation : make transition clear by using union types.
let win player score =
match player, score with
| _, Game p -> Game p
| Left, Advantage Left | Left, Points (Forty, _) -> Game Left
| Right, Advantage Right | Right, Points (_, Forty) -> Game Right
| Left, Points (Love, Fifteen) | Right, Points(Fifteen, Love) -> FifteenAll
| Left, FifteenAll -> Points (Thirty, Fifteen)
| Right, FifteenAll -> Points (Fifteen, Thirty)
| Left, Points (Fifteen, Thirty) | Right, Points (Thirty, Fifteen) -> ThirtyAll
| Left, ThirtyAll -> Points (Forty, Thirty)
| Right, ThirtyAll -> Points (Thirty, Forty)
| Left, Points(Thirty, Forty) | Right, Points(Forty, Thirty) -> Deuce
| p, Deuce -> Advantage p
| Left, Advantage Right | Right, Advantage Left -> Deuce
| Left, Points (l, r) -> Points (Point.next l, r)
| Right, Points (l, r) -> Points (l, Point.next r)
// REPL unit tests ////////////////////////////////////////////////////////////////////////
// TIP / REPL style : How about bringing the power of TDD / Unit testing in the REPL ?
let shouldEqual actual expected =
if actual = expected then actual
else failwithf "expected:\n%A\ngot\n%A" expected actual
// Left player always wins
Score.start
|> Player.win Left |> shouldEqual (Points(Fifteen, Love))
|> Player.win Left |> shouldEqual (Points(Thirty, Love))
|> Player.win Left |> shouldEqual (Points(Forty, Love))
|> Player.win Left |> shouldEqual (Game Left)
// Left wins the game but the right player is close to win
Score.start
|> Player.win Left |> shouldEqual (Points(Fifteen, Love))
|> Player.win Right |> shouldEqual FifteenAll
|> Player.win Left |> shouldEqual (Points (Thirty, Fifteen))
|> Player.win Right |> shouldEqual ThirtyAll
|> Player.win Left |> shouldEqual (Points(Forty, Thirty))
|> Player.win Right |> shouldEqual Deuce
|> Player.win Left |> shouldEqual (Advantage Left)
|> Player.win Right |> shouldEqual Deuce
|> Player.win Right |> shouldEqual (Advantage Right)
|> Player.win Left |> shouldEqual Deuce
|> Player.win Left |> shouldEqual (Advantage Left)
|> Player.win Left |> shouldEqual (Game Left)
// Now exact the same game but with Right as a winner :)
Score.start
|> Player.win Right |> shouldEqual (Points(Love, Fifteen))
|> Player.win Left |> shouldEqual FifteenAll
|> Player.win Right |> shouldEqual (Points (Fifteen, Thirty))
|> Player.win Left |> shouldEqual ThirtyAll
|> Player.win Right |> shouldEqual (Points(Thirty, Forty))
|> Player.win Left |> shouldEqual Deuce
|> Player.win Right |> shouldEqual (Advantage Right)
|> Player.win Left |> shouldEqual Deuce
|> Player.win Left |> shouldEqual (Advantage Left)
|> Player.win Right |> shouldEqual Deuce
|> Player.win Right |> shouldEqual (Advantage Right)
|> Player.win Right |> shouldEqual (Game Right)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment