How do I make illegal behavior unexecutable?
Summary:
Since starting my journey to learn F#, I am learning about Type-Driven Design and Property-based Testing. As a result, I fell in love with the idea of making illegal states unrepresentable.
But what I would really like to do is to make illegal behavior unexecutable.
I am learning F# by writing a BlackJack game. As a result, I want to ensure that when a dealer distributes cards, that the dealer can only deal an "initial hand" or a "hit". All other distributions of cards are illegal.
In C#, I would implement the Strategy Pattern and thus, create a DealHandCommand and a DealHitCommand. Then I would hard-code a constant integer value for the number of cards to deal-out (per strategy).
DealHandCommand = 2 cards
DealHitCommand = 1 card
Based on these strategies, I would then implement a state-machine to represent a session of a BlackJack game. Hence, after I deal the initial hand (i.e. DealHandCommand), I perform a state transition in which future deals can only execute the "DealHitCommand".
Specifically, does it make sense to implement a state-machine within a hybrid-functional language in order to achieve illegal behavior as unexecutable?
It's easy to implement a state machine in F#. It usually follows a three-step process, with the third step being optional:
In this case it sounds to me like there are two states:
That suggests this Deal
discriminated union:
type Deal = Hand of Card * Card | Hit of Card
Also, define what a Game
is:
type Game = Game of Deal list
Notice the use of a single-case discriminated union; there's a reason for that.
Now define a function that transitions from each state to a Game
.
It turns out that you can't transition from any game state to the Hand
case, because a Hand
is what starts a new game. On the other hand (pun intended) you need to supply the cards that go into the hand:
let init c1 c2 = Game [Hand (c1, c2)]
The other case is when a game is in progress, you should only allow Hit
, but not Hand
, so define this transition:
let hit (Game deals) card = Game (Hit card :: deals)
As you can see, the hit
function requires you to pass in an existing Game
.
What prevents a client from creating an invalid Game
value, e.g. [Hand; Hit; Hand; Hit; Hit]
?
You can encapsulate the above state machine with a signature file:
BlackJack.fsi:
type Deal
type Game
val init : Card -> Card -> Game
val hit : Game -> Card -> Game
val card : Deal -> Card list
val cards : Game -> Card list
Here, the types Deal
and Game
are declared, but their 'constructors' aren't. This means that you can't directly create values of these types. This, for example, doesn't compile:
let g = BlackJack.Game []
The error given is:
error FS0039: The value, constructor, namespace or type 'Game' is not defined
The only way to create a Game
value is to call a function that creates it for you:
let g =
BlackJack.init
{ Face = Ace; Suit = Spades }
{ Face = King; Suit = Diamonds }
This also enables you to continue the game:
let g' = BlackJack.hit g { Face = Two; Suit = Spades }
You may have noticed that the above signature file also defines two functions to get the cards out of Game
and Deal
values. Here are the implementations:
let card = function
| Hand (c1, c2) -> [c1; c2]
| Hit c -> [c]
let cards (Game deals) = List.collect card deals
A client can use them like this:
> let cs = g' |> BlackJack.cards;;
>
val cs : Card list = [{Suit = Spades;
Face = Two;};
{Suit = Spades;
Face = Ace;};
{Suit = Diamonds;
Face = King;}]
Notice that this approach is mostly structural; there are few moving parts.
These are the files used above:
Cards.fs:
namespace Ploeh.StackOverflow.Q34042428.Cards
type Suit = Diamonds | Hearts | Clubs | Spades
type Face =
| Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten
| Jack | Queen | King | Ace
type Card = { Suit: Suit; Face: Face }
BlackJack.fsi:
module Ploeh.StackOverflow.Q34042428.Cards.BlackJack
type Deal
type Game
val init : Card -> Card -> Game
val hit : Game -> Card -> Game
val card : Deal -> Card list
val cards : Game -> Card list
BlackJack.fs:
module Ploeh.StackOverflow.Q34042428.Cards.BlackJack
open Ploeh.StackOverflow.Q34042428.Cards
type Deal = Hand of Card * Card | Hit of Card
type Game = Game of Deal list
let init c1 c2 = Game [Hand (c1, c2)]
let hit (Game deals) card = Game (Hit card :: deals)
let card = function
| Hand (c1, c2) -> [c1; c2]
| Hit c -> [c]
let cards (Game deals) = List.collect card deals
Client.fs:
module Ploeh.StackOverflow.Q34042428.Cards.Client
open Ploeh.StackOverflow.Q34042428.Cards
let g =
BlackJack.init
{ Face = Ace; Suit = Spades }
{ Face = King; Suit = Diamonds }
let g' = BlackJack.hit g { Face = Two; Suit = Spades }
let cs = g' |> BlackJack.cards
a hit is one more card right?
If so then just use two types:
type HandDealt = Dealt of Card * Card
type Playing = Playing of Cards
Then instead of Commands you have simple functions:
dealHand :: Card * Card -> HandDealt
start :: HandDealt -> Playing
dealAnother :: Playing -> Card -> Playing
this way you can only follow a certain behavior and it's statically checked.
of couse you probably want to extend those types to multiple players but I think you get what I am going to
PS: maybe you even like to skip the HandDealt
/ start
phase (if you don't need the middle phase for things like betting/splitting/etc. - but please mind that I have no clue about blackjack):
dealHand :: Card * Card -> Playing
dealAnother :: Playing -> Card -> Playing
it's up to you
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With