Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I ensure that illegal behavior is unexecutable?

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?

like image 318
Scott Nimrod Avatar asked Dec 02 '15 12:12

Scott Nimrod


2 Answers

It's easy to implement a state machine in F#. It usually follows a three-step process, with the third step being optional:

  1. Define a Discriminated Union with a case for each state
  2. Define a transition function for each case
  3. Optional: implement all the rest of the code

Step 1

In this case it sounds to me like there are two states:

  • An initial Hand with two cards
  • A Hit with an extra card

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.

Step 2

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.

Step 3

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.

Appendix

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
like image 177
Mark Seemann Avatar answered Sep 29 '22 10:09

Mark Seemann


a hit is one more card right?

If so then just use two types:

  • type HandDealt = Dealt of Card * Card
  • and type Playing = Playing of Cards
  • (maybe more - depends on what you want).

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

like image 39
Random Dev Avatar answered Sep 29 '22 12:09

Random Dev