Chessie


Combining terminal and non-terminal errors

One feature of programming with "two-track" data is that once a value is on the failure track operations against it may be skipped if they expect a value on the passing track. However, this is not always the desired behavior. In many cases, we will want to perform mulitple operations on a value and record any unusual or interesting results, but still keep thing on the passing track. One obvious scenario is when we want to validate multiple pieces of input (such as might be received from an HTTP request).

1: 
2: 
3: 
4: 
type Applicant = 
  { FullName      :string * string
    DateOfBirth   :DateTime
    FavoriteColor :Color option }

Given some aggregate user-supplied data, we want to check each datum.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
let checkName getName msg request =
  let name = getName request
  if String.IsNullOrWhiteSpace name
    then  // note the invalid datum, but stay on the _passing_ track
          request |> warn msg
    else  // no issues, proceed on the "happy path"
          request |> pass

let checkFirstName  = checkName (fun {FullName = (name,_)} -> name) "First name is missing"
let checkLastName   = checkName (fun {FullName = (_,name)} -> name) "Last name is missing"

let checkAge request =
  let dob   = request.DateOfBirth
  let diff  = DateTime.Today.Subtract dob
  if  diff  < TimeSpan.FromDays (18.0 * 365.0)
    then  // note the invalid datum, but stay on the _passing_ track
          request |> warn "DateOfBirth is too recent"
    else  // no issues, proceed on the "happy path"
          request |> pass

Now we can combine our individual checks, returning the original value along-side any issues.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
let checkApplicant request =  
  request 
  |> checkAge
  |> bind checkFirstName
  |> bind checkLastName

let processRequest request = 
  match checkApplicant request with
  | Pass  _       ->  printfn "All Good!!!"
  | Warn (_,log)  ->  printfn "Got some issues:"
                      for msg in log do printfn "  %s" msg
  | _             ->  printfn "Something went horribly wrong."

            
// good request
processRequest {FullName      = "John","Smith" 
                DateOfBirth   = DateTime (1995,12,21)
                FavoriteColor = None}
// bad request
processRequest {FullName      = "Beck","" 
                DateOfBirth   = DateTime (2005,04,13)
                FavoriteColor = Some Color.Gold}
No output has been produced.

We can also mixed warnings in with operations that are terminal. In other words, we can still build workflows where certain operations switch the data over to the failure track.

 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: 
let disallowPink request =
  match request.FavoriteColor with
  | Some c // no idea why we're being mean to this particular color
    when c = Color.Pink -> fail "Get outta here with that color!"
  | _                   -> pass request

let recheckApplicant request =
  request
  |> checkAge
  |> bind disallowPink
  |> bind checkFirstName
  |> bind checkLastName

let reportMessages request =
  match recheckApplicant request with
  | Pass  _       ->  printfn "Nothing to report"
  | Warn (_,log)  ->  printfn "Got some issues:"
                      for msg in log do printfn "  %s" msg
  | Fail  errors  ->  printfn "Got errors:"
                      for msg in errors do printfn "  %s" msg

// terminal request
reportMessages {FullName      = "John","Smith" 
                DateOfBirth   = DateTime (1995,12,21)
                FavoriteColor = Some Color.Pink}

// non-terminal request with warnings
reportMessages {FullName      = "","Smith" 
                DateOfBirth   = DateTime (1995,12,21)
                FavoriteColor = Some Color.Green}
// good request
reportMessages {FullName      = "Bob","Smith" 
                DateOfBirth   = DateTime (1995,12,21)
                FavoriteColor = Some Color.Green}
No output has been produced.

In effect, we've turned "two-track" data into "three-track" data. But we can also flip this around. That's is, run operations over the data accumlating warnings. Then at the end, if we have any messages at all, switch to the failure track.

 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: 
let warnOnNoColor request =
  match request.FavoriteColor with
  | None    -> request |> warn "No color provided"
  | Some _  -> pass request

let ``checkApplicant (again)`` request =
  request
  |> checkAge
  |> bind checkFirstName
  |> bind checkLastName
  |> bind warnOnNoColor

let ``processRequest (again)`` request =
  // turn any warning messages into failure messages
  let result =  request
                |> ``checkApplicant (again)``
                |> failOnWarnings
  // now we only have 2 tracks on which the data may lay
  match result with
  | Fail errors ->  for x in errors do printfn "ERROR! %s" x
  | _           ->  printfn "SUCCESS!!!"

// terminal request
``processRequest (again)`` {FullName      = "","" 
                            DateOfBirth   = DateTime.Today
                            FavoriteColor = None}
No output has been produced.
namespace Chessie
namespace Chessie.ErrorHandling
namespace System
namespace System.Drawing
type Applicant =
  {FullName: string * string;
   DateOfBirth: DateTime;
   FavoriteColor: Color option;}

Full name: Pass-warn-fail.Applicant
Applicant.FullName: string * string
Multiple items
val string : value:'T -> string

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

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
Applicant.DateOfBirth: DateTime
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 10 overloads
    member Add : value:TimeSpan -> DateTime
    member AddDays : value:float -> DateTime
    member AddHours : value:float -> DateTime
    member AddMilliseconds : value:float -> DateTime
    member AddMinutes : value:float -> DateTime
    member AddMonths : months:int -> DateTime
    member AddSeconds : value:float -> DateTime
    member AddTicks : value:int64 -> DateTime
    member AddYears : value:int -> DateTime
    ...
  end

Full name: System.DateTime

--------------------
DateTime()
   (+0 other overloads)
DateTime(ticks: int64) : unit
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : unit
   (+0 other overloads)
Applicant.FavoriteColor: Color option
type Color =
  struct
    member A : byte
    member B : byte
    member Equals : obj:obj -> bool
    member G : byte
    member GetBrightness : unit -> float32
    member GetHashCode : unit -> int
    member GetHue : unit -> float32
    member GetSaturation : unit -> float32
    member IsEmpty : bool
    member IsKnownColor : bool
    ...
  end

Full name: System.Drawing.Color
type 'T option = Option<'T>

Full name: Microsoft.FSharp.Core.option<_>
val checkName : getName:('a -> string) -> msg:'b -> request:'a -> Result<'a,'b>

Full name: Pass-warn-fail.checkName
val getName : ('a -> string)
val msg : 'b
val request : 'a
val name : string
Multiple items
type String =
  new : value:char -> string + 7 overloads
  member Chars : int -> char
  member Clone : unit -> obj
  member CompareTo : value:obj -> int + 1 overload
  member Contains : value:string -> bool
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EndsWith : value:string -> bool + 2 overloads
  member Equals : obj:obj -> bool + 2 overloads
  member GetEnumerator : unit -> CharEnumerator
  member GetHashCode : unit -> int
  ...

Full name: System.String

--------------------
String(value: nativeptr<char>) : unit
String(value: nativeptr<sbyte>) : unit
String(value: char []) : unit
String(c: char, count: int) : unit
String(value: nativeptr<char>, startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int) : unit
String(value: char [], startIndex: int, length: int) : unit
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : unit
String.IsNullOrWhiteSpace(value: string) : bool
val warn : msg:'TMessage -> x:'TSuccess -> Result<'TSuccess,'TMessage>

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

Full name: Chessie.ErrorHandling.Trial.pass
val checkFirstName : (Applicant -> Result<Applicant,string>)

Full name: Pass-warn-fail.checkFirstName
val checkLastName : (Applicant -> Result<Applicant,string>)

Full name: Pass-warn-fail.checkLastName
val checkAge : request:Applicant -> Result<Applicant,string>

Full name: Pass-warn-fail.checkAge
val request : Applicant
val dob : DateTime
val diff : TimeSpan
property DateTime.Today: DateTime
DateTime.Subtract(value: TimeSpan) : DateTime
DateTime.Subtract(value: DateTime) : TimeSpan
Multiple items
type TimeSpan =
  struct
    new : ticks:int64 -> TimeSpan + 3 overloads
    member Add : ts:TimeSpan -> TimeSpan
    member CompareTo : value:obj -> int + 1 overload
    member Days : int
    member Duration : unit -> TimeSpan
    member Equals : value:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member Hours : int
    member Milliseconds : int
    member Minutes : int
    ...
  end

Full name: System.TimeSpan

--------------------
TimeSpan()
TimeSpan(ticks: int64) : unit
TimeSpan(hours: int, minutes: int, seconds: int) : unit
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : unit
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : unit
TimeSpan.FromDays(value: float) : TimeSpan
val checkApplicant : request:Applicant -> Result<Applicant,string>

Full name: Pass-warn-fail.checkApplicant
val bind : f:('a -> Result<'b,'c>) -> result:Result<'a,'c> -> Result<'b,'c>

Full name: Chessie.ErrorHandling.Trial.bind
val processRequest : request:Applicant -> unit

Full name: Pass-warn-fail.processRequest
active recognizer Pass: Result<'a,'b> -> Choice<'a,('a * 'b list),'b list>

Full name: Chessie.ErrorHandling.Trial.( |Pass|Warn|Fail| )
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
active recognizer Warn: Result<'a,'b> -> Choice<'a,('a * 'b list),'b list>

Full name: Chessie.ErrorHandling.Trial.( |Pass|Warn|Fail| )
val log : string list
val msg : string
union case Option.None: Option<'T>
union case Option.Some: Value: 'T -> Option<'T>
property Color.Gold: Color
val disallowPink : request:Applicant -> Result<Applicant,string>

Full name: Pass-warn-fail.disallowPink
val c : Color
property Color.Pink: Color
val fail : msg:'Message -> Result<'TSuccess,'Message>

Full name: Chessie.ErrorHandling.Trial.fail
val recheckApplicant : request:Applicant -> Result<Applicant,string>

Full name: Pass-warn-fail.recheckApplicant
val reportMessages : request:Applicant -> unit

Full name: Pass-warn-fail.reportMessages
active recognizer Fail: Result<'a,'b> -> Choice<'a,('a * 'b list),'b list>

Full name: Chessie.ErrorHandling.Trial.( |Pass|Warn|Fail| )
val errors : string list
property Color.Green: Color
val warnOnNoColor : request:Applicant -> Result<Applicant,string>

Full name: Pass-warn-fail.warnOnNoColor
val ( checkApplicant (again) ) : request:Applicant -> Result<Applicant,string>

Full name: Pass-warn-fail.( checkApplicant (again) )
val ( processRequest (again) ) : request:Applicant -> unit

Full name: Pass-warn-fail.( processRequest (again) )
val result : Result<Applicant,string>
val failOnWarnings : result:Result<'a,'b> -> Result<'a,'b>

Full name: Chessie.ErrorHandling.Trial.failOnWarnings
val x : string
Fork me on GitHub