Chessie


Using Chessie for Railway-oriented programming (ROP)

This tutorial is based on an article about Railway-oriented programming by Scott Wlaschin.

Additional resources:

  • Railway Oriented Programming by Scott Wlaschin - A functional approach to error handling

We start by referencing Chessie and opening the ErrorHandling module:

1: 
2: 
3: 
#r "Chessie.dll"

open Chessie.ErrorHandling

Now we define some simple validation functions:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
type Request = 
    { Name : string
      EMail : string }

let validateInput input = 
    if input.Name = "" then fail "Name must not be blank"
    elif input.EMail = "" then fail "Email must not be blank"
    else ok input // happy path

let validate1 input = 
    if input.Name = "" then fail "Name must not be blank"
    else ok input

let validate2 input = 
    if input.Name.Length > 50 then fail "Name must not be longer than 50 chars"
    else ok input

let validate3 input = 
    if input.EMail = "" then fail "Email must not be blank"
    else ok input

let combinedValidation = 
    // connect the two-tracks together
    validate1
    >> bind validate2
    >> bind validate3

Let's use these with some basic combinators:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
52: 
53: 
54: 
55: 
56: 
57: 
58: 
59: 
{ Name = ""; EMail = "" }
|> combinedValidation
val it : Chessie.ErrorHandling.Result<Request,string> =
  Bad ["Name must not be blank"]
    
{ Name = "Scott"; EMail = "" }
|> combinedValidation
val it : Chessie.ErrorHandling.Result<Request,string> =
  bad ["Email must not be blank"]

{ Name = "ScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScottScott"
  EMail = "" }
|> combinedValidation
val it : Chessie.ErrorHandling.Result<Request,string> =
  Bad ["Name must not be longer than 50 chars" ]

{ Name = "Scott"; EMail = "scott@chessie.com" }
|> combinedValidation
|> returnOrFail
val it : Request = {Name = "Scott"; EMail = "scott@chessie.com";}


let canonicalizeEmail input = { input with EMail = input.EMail.Trim().ToLower() }

let usecase = 
    combinedValidation
    >> (lift canonicalizeEmail)

{ Name = "Scott"; EMail = "SCOTT@CHESSIE.com" }
|> usecase
|> returnOrFail
val it : Request = {Name = "Scott"; EMail = "scott@chessie.com";}

{ Name = ""; EMail = "SCOTT@CHESSIE.com" }
|> usecase
val it : Result<Request,string> = Bad ["Name must not be blank"]

// a dead-end function    
let updateDatabase input =
   ()   // dummy dead-end function for now


let log twoTrackInput = 
    let success(x,msgs) = printfn "DEBUG. Success so far."
    let failure msgs = printf "ERROR. %A" msgs
    eitherTee success failure twoTrackInput 

let usecase2 = 
    usecase
    >> (successTee updateDatabase)
    >> log


{ Name = "Scott"; EMail = "SCOTT@CHESSIE.com" }
|> usecase2
|> returnOrFail
DEBUG. Success so far.
val it : Request = {Name = "Scott";
                    EMail = "scott@chessie.com";}
namespace Chessie
namespace Chessie.ErrorHandling
type Request =
  {Name: string;
   EMail: string;}

Full name: Railway.Request
Request.Name: 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
Request.EMail: string
val validateInput : input:Request -> Result<Request,string>

Full name: Railway.validateInput
val input : Request
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 validate1 : input:Request -> Result<Request,string>

Full name: Railway.validate1
val validate2 : input:Request -> Result<Request,string>

Full name: Railway.validate2
property System.String.Length: int
val validate3 : input:Request -> Result<Request,string>

Full name: Railway.validate3
val combinedValidation : (Request -> Result<Request,string>)

Full name: Railway.combinedValidation
val bind : f:('a -> Result<'b,'c>) -> result:Result<'a,'c> -> Result<'b,'c>

Full name: Chessie.ErrorHandling.Trial.bind
val returnOrFail : result:Result<'a,'b> -> 'a

Full name: Chessie.ErrorHandling.Trial.returnOrFail
val canonicalizeEmail : input:Request -> Request

Full name: Railway.canonicalizeEmail
System.String.Trim() : string
System.String.Trim([<System.ParamArray>] trimChars: char []) : string
val usecase : (Request -> Result<Request,string>)

Full name: Railway.usecase
val lift : f:('a -> 'b) -> result:Result<'a,'c> -> Result<'b,'c>

Full name: Chessie.ErrorHandling.Trial.lift
val updateDatabase : input:'a -> unit

Full name: Railway.updateDatabase
val input : 'a
val log : twoTrackInput:Result<'a,'b> -> Result<'a,'b>

Full name: Railway.log
val twoTrackInput : Result<'a,'b>
val success : ('c * 'd -> unit)
val x : 'c
val msgs : 'd
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val failure : ('c -> unit)
val msgs : 'c
val printf : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printf
val eitherTee : fSuccess:('c * 'd list -> unit) -> fFailure:('d list -> unit) -> result:Result<'c,'d> -> Result<'c,'d>

Full name: Chessie.ErrorHandling.Trial.eitherTee
val usecase2 : (Request -> Result<Request,string>)

Full name: Railway.usecase2
val successTee : f:('a * 'b list -> unit) -> result:Result<'a,'b> -> Result<'a,'b>

Full name: Chessie.ErrorHandling.Trial.successTee
Fork me on GitHub