Chessie


A Tale of 3 Nightclubs

This F# tutorial is based on a Scalaz tutorial by Chris Marshall and was originally ported to fsharpx by Mauricio Scheffer.

Additional resources:

Part Zero : 10:15 Saturday Night

We start by referencing Chessie and opening the ErrorHandling module and define a simple domain for nightclubs:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
#r "Chessie.dll"

open Chessie.ErrorHandling

type Sobriety = 
    | Sober
    | Tipsy
    | Drunk
    | Paralytic
    | Unconscious

type Gender = 
    | Male
    | Female

type Person = 
    { Gender : Gender
      Age : int
      Clothes : string Set
      Sobriety : Sobriety }

Now we define some validation methods that all nightclubs will perform:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
module Club = 
    let checkAge (p : Person) = 
        if p.Age < 18 then fail "Too young!"
        elif p.Age > 40 then fail "Too old!"
        else ok p
    
    let checkClothes (p : Person) = 
        if p.Gender = Male && not (p.Clothes.Contains "Tie") then fail "Smarten up!"
        elif p.Gender = Female && p.Clothes.Contains "Trainers" then fail "Wear high heels"
        else ok p
    
    let checkSobriety (p : Person) = 
        match p.Sobriety with
        | Drunk | Paralytic | Unconscious -> fail "Sober up!"
        | _ -> ok p

Part One : Clubbed to Death

Now let's compose some validation checks via syntactic sugar:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
module ClubbedToDeath =
    open Club
    
    let costToEnter p =
        trial {
            let! a = checkAge p
            let! b = checkClothes a
            let! c = checkSobriety b
            return 
                match c.Gender with
                | Female -> 0m
                | Male -> 5m
        }

Let's see how the validation works in action:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
let Ken = { Person.Gender = Male; Age = 28; Clothes = set ["Tie"; "Shirt"]; Sobriety = Tipsy }
let Dave = { Person.Gender = Male; Age = 41; Clothes = set ["Tie"; "Jeans"]; Sobriety = Sober }
let Ruby = { Person.Gender = Female; Age = 25; Clothes = set ["High heels"]; Sobriety = Tipsy }

ClubbedToDeath.costToEnter Dave 
val it : Chessie.ErrorHandling.Result<decimal,string> = Bad ["Too old!"]

ClubbedToDeath.costToEnter Ken
val it : Result<decimal,string> = Ok (5M,[])

ClubbedToDeath.costToEnter Ruby
val it : Result<decimal,string> = Ok (0M,[])

ClubbedToDeath.costToEnter { Ruby with Age = 17 } 
val it : Chessie.ErrorHandling.Result<decimal,string> = Bad ["Too young!"]

The thing to note here is how the Validations can be composed together in a computation expression. The type system is making sure that failures flow through your computation in a safe manner.

Part Two : Club Tropicana

Part One showed monadic composition, which from the perspective of Validation is fail-fast. That is, any failed check short-circuits subsequent checks. This nicely models nightclubs in the real world, as anyone who has dashed home for a pair of smart shoes and returned, only to be told that your tie does not pass muster, will attest.

But what about an ideal nightclub? One that tells you everything that is wrong with you.

Applicative functors to the rescue!

Let's compose some validation checks that accumulate failures :

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
module ClubTropicana = 
    open Club

    let costToEnter p =
        trial {
            let a = checkAge p
            let b = checkClothes p
            let c = checkSobriety p

            let! result::_ =  [a;b;c] |> collect
            
            return 
                match result.Gender with
                | Female -> 0m
                | Male -> 7.5m
        }

The usage is the same as above except that as a result we will get either a success or a list of accumulated error messages from all the checks.

Dave tried the second nightclub after a few more drinks in the pub:

1: 
2: 
3: 
4: 
5: 
let daveParalytic = { Person.Gender = Male; Age = 41; Clothes = set ["Tie"; "Shirt"]; Sobriety = Paralytic }

ClubTropicana.costToEnter daveParalytic
// val it : Result<decimal,string> = Error: Too old!
// Sober up!

So, what have we done? Well, with a tiny change (and no changes to the individual checks themselves), we have completely changed the behaviour to accumulate all errors, rather than halting at the first sign of trouble. Imagine trying to do this using exceptions, with ten checks.

Part Three : Gay bar

And for those wondering how to do this with a very long list of checks here is a solution:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
module GayBar = 
    open Club

    let checkGender (p : Person) = 
        if p.Gender = Male then ok p 
        else fail "Men Only"

    let costToEnter p =
        trial {
            let! result::_ =  
                [checkGender; checkAge; checkClothes; checkSobriety] 
                |> List.map(fun f -> f p)
                |> collect
            return 
                match result.Gender with
                | Female -> 0m
                | Male -> 7.5m
        }

The usage is the same as above:

1: 
2: 
3: 
4: 
5: 
6: 
let person = { Person.Gender = Male; Age = 59; Clothes = set ["Jeans"]; Sobriety = Paralytic }

GayBar.costToEnter person
// val it : Result<decimal,string> = Error: Too old!
// Smarten up!
// Sober up!
namespace Chessie
namespace Chessie.ErrorHandling
type Sobriety =
  | Sober
  | Tipsy
  | Drunk
  | Paralytic
  | Unconscious

Full name: A-tale-of-3-nightclubs-fsharp.Sobriety
union case Sobriety.Sober: Sobriety
union case Sobriety.Tipsy: Sobriety
union case Sobriety.Drunk: Sobriety
union case Sobriety.Paralytic: Sobriety
union case Sobriety.Unconscious: Sobriety
type Gender =
  | Male
  | Female

Full name: A-tale-of-3-nightclubs-fsharp.Gender
union case Gender.Male: Gender
union case Gender.Female: Gender
type Person =
  {Gender: Gender;
   Age: int;
   Clothes: Set<string>;
   Sobriety: Sobriety;}

Full name: A-tale-of-3-nightclubs-fsharp.Person
Multiple items
Person.Gender: Gender

--------------------
type Gender =
  | Male
  | Female

Full name: A-tale-of-3-nightclubs-fsharp.Gender
Person.Age: int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
Person.Clothes: Set<string>
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
Multiple items
module Set

from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> =
  interface IComparable
  interface IEnumerable
  interface IEnumerable<'T>
  interface ICollection<'T>
  new : elements:seq<'T> -> Set<'T>
  member Add : value:'T -> Set<'T>
  member Contains : value:'T -> bool
  override Equals : obj -> bool
  member IsProperSubsetOf : otherSet:Set<'T> -> bool
  member IsProperSupersetOf : otherSet:Set<'T> -> bool
  ...

Full name: Microsoft.FSharp.Collections.Set<_>

--------------------
new : elements:seq<'T> -> Set<'T>
Multiple items
Person.Sobriety: Sobriety

--------------------
type Sobriety =
  | Sober
  | Tipsy
  | Drunk
  | Paralytic
  | Unconscious

Full name: A-tale-of-3-nightclubs-fsharp.Sobriety
val checkAge : p:Person -> Result<Person,string>

Full name: A-tale-of-3-nightclubs-fsharp.Club.checkAge
val p : Person
val fail : msg:'Message -> Result<'TSuccess,'Message>

Full name: Chessie.ErrorHandling.Trial.fail
val ok : x:'TSuccess -> Result<'TSuccess,'TMessage>

Full name: Chessie.ErrorHandling.Trial.ok
val checkClothes : p:Person -> Result<Person,string>

Full name: A-tale-of-3-nightclubs-fsharp.Club.checkClothes
Person.Gender: Gender
val not : value:bool -> bool

Full name: Microsoft.FSharp.Core.Operators.not
member Set.Contains : value:'T -> bool
val checkSobriety : p:Person -> Result<Person,string>

Full name: A-tale-of-3-nightclubs-fsharp.Club.checkSobriety
Person.Sobriety: Sobriety
module Club

from A-tale-of-3-nightclubs-fsharp
val costToEnter : p:Person -> Result<decimal,string>

Full name: A-tale-of-3-nightclubs-fsharp.ClubbedToDeath.costToEnter
val trial : TrialBuilder

Full name: Chessie.ErrorHandling.Trial.trial
val a : Person
val b : Person
val c : Person
val Ken : Person

Full name: A-tale-of-3-nightclubs-fsharp.Ken
val set : elements:seq<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.set
val Dave : Person

Full name: A-tale-of-3-nightclubs-fsharp.Dave
val Ruby : Person

Full name: A-tale-of-3-nightclubs-fsharp.Ruby
module ClubbedToDeath

from A-tale-of-3-nightclubs-fsharp
val costToEnter : p:Person -> Result<decimal,string>

Full name: A-tale-of-3-nightclubs-fsharp.ClubTropicana.costToEnter
val a : Result<Person,string>
val b : Result<Person,string>
val c : Result<Person,string>
val result : Person
val collect : xs:seq<Result<'a,'b>> -> Result<'a list,'b>

Full name: Chessie.ErrorHandling.Trial.collect
val daveParalytic : Person

Full name: A-tale-of-3-nightclubs-fsharp.daveParalytic
module ClubTropicana

from A-tale-of-3-nightclubs-fsharp
val checkGender : p:Person -> Result<Person,string>

Full name: A-tale-of-3-nightclubs-fsharp.GayBar.checkGender
val costToEnter : p:Person -> Result<decimal,string>

Full name: A-tale-of-3-nightclubs-fsharp.GayBar.costToEnter
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val map : mapping:('T -> 'U) -> list:'T list -> 'U list

Full name: Microsoft.FSharp.Collections.List.map
val f : (Person -> Result<Person,string>)
val person : Person

Full name: A-tale-of-3-nightclubs-fsharp.person
module GayBar

from A-tale-of-3-nightclubs-fsharp
Fork me on GitHub