FsUno.Prod


The Digit type and domain constraints

tldr; Model domain constraints inside types. Use model constraints or exceptions for invariants that should be ensured at all time by the system.

2016-08-24

Primitive obsession

There a several ways to model digits for this game, the simplest one is to use an integer.

1: 
2: 
3: 
4: 
    type Digit = int
    type Card = 
        | Digit of Digit * Color
      //| ... other card types

It is quite flacky modeling. What would this represent ?

1: 
2: 
3: 
    (10,Blue) 
    (Int32.MaxValue, Yellow)
    (-3, Green)

It's such a recurrent smell it has a name: Primitive Obsession.

The main problem is that the code will have to check the validity of the value in various places. Any missed check leading to potential bugs and in some cases, security risks.

The solution is to restrain the range of values from start, so that we don't have to think about it later.

Wrapping type

My second option was to wrap the value in a more specific type to avoid bogus values:

1: 
2: 
3: 
4: 
5: 
6: 
    type Digit = private Digit of int

    let digit n =
        if n < 0 || n > 9 then
            invalidArg "n" "Digit should be between 0 and 9."
        Digit n

Now, once a digit value has been constructed, I'm sure it's valid.

Far better.

It's no real problem to use exception here. Functions that work on Digit can safely trust it has been build correctly, so no exception will be raised except in case of bugs. But in this case, you don't want to go any further - Stop the Press ! - and if you have plugged a bug reporter - and you should - you'll receive a notification. Then fix the bug and release.

It's fine to raise exceptions for things that should not occure at all.

The other place where it could happen is at the system boundary: input fields, APIs.

For input fields, you should provide a field that restrict the user to enter valid input. In the digit case, a dropdown selector or even better, a list of clickable cards. It's a good way to guide the user into providing valid input.

Another way is to provide a text field and using a try parse function:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
    type Result<'t,'e> =
        | Ok of 't
        | Failure of 'e

    let tryParse input =
        match Int32.TryParse input with
        | false, _ -> Failure "The value should be an integer"
        | true, n ->
            if n < 0 || n > 9 then
                Failure "The value should be between 0 and 9"
            else
                Ok (Digit n)

Any way, even if you propose a restricted set of values, never assume the client sent a valid input. Always check the value at input point using the digit constructor function.

Even more restrictive

In the case of Uno, the value of the card is almost never used for its value, cards just have to be of equal value. It could as well be drawings like Square, Circle, Star...

In this case the type would probably modeled as:

1: 
2: 
3: 
4: 
5: 
    type Drawing =
        | Square
        | Circle
        | Star
    //  | ... other drawings

This type works perfectly for equality. Enough for me.

And it adds a futher constraint: I cannot not represent at compile time an invalid value.

The previous version allowed me to write digit -3, which will fail at runtime, but will compile. So I'll need unit tests to validate the digit constructor function. And hope that no one will do something foolish in the code that will result in digit n where n doen's pass the check.

With the following version, it's just not possible:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
type Digit =
    | Zero
    | One
    | Two
    | Three
    | Four
    | Five
    | Six
    | Seven
    | Height
    | Nine

There is a single place where I'll need validation. When parsing it on the boundary, which is usually done inside deserialization:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let cases = 
    [ Zero; One; Two;   Three;  Four
      Five; Six; Seven; Height; Nine ]
    |> List.map (fun d -> sprintf "%A" d, d)
    |> Map.ofList
    
let tryParse input =
    match Map.tryFind input cases with
    | Some digit -> Ok digit
    | None -> Failure "Unknown digit"

We can the try:

1: 
2: 
tryParse "Seven" // returns Ok Seven
tryParse "Bogus" // returns Failure "Unknown digit"

The only place where the actual value is needed is for score, where a simple function is needed:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let score digit =
    match digit with
    | Zero   -> 0
    | One    -> 1
    | Two    -> 2
    | Three  -> 3
    | Four   -> 4
    | Five   -> 5
    | Six    -> 6
    | Seven  -> 7
    | Height -> 8
    | Nine   -> 9

The pattern matching is a total match, so we're sure we missed no case.

Of course, for bigger enumerations, like value between 1 and 100, the wrapping type is probably a better option.

namespace System
type Color =
  | Red
  | Green
  | Blue
  | Yellow

Full name: The Digit type.Color
union case Color.Red: Color
union case Color.Green: Color
union case Color.Blue: Color
union case Color.Yellow: Color
type Digit = int

Full name: The Digit type.WithIntegers.Digit
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<_>
type Card = | Digit of Digit * Color

Full name: The Digit type.WithIntegers.Card
Multiple items
union case Card.Digit: Digit * Color -> Card

--------------------
type Digit = int

Full name: The Digit type.WithIntegers.Digit
type Int32 =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    static val MaxValue : int
    static val MinValue : int
    static member Parse : s:string -> int + 3 overloads
    static member TryParse : s:string * result:int -> bool + 1 overload
  end

Full name: System.Int32
field int.MaxValue = 2147483647
Multiple items
union case Digit.Digit: int -> Digit

--------------------
type Digit = private | Digit of int

Full name: The Digit type.WrappingType.Digit
val digit : n:int -> Digit

Full name: The Digit type.WrappingType.digit
val n : int
val invalidArg : argumentName:string -> message:string -> 'T

Full name: Microsoft.FSharp.Core.Operators.invalidArg
type Result<'t,'e> =
  | Ok of 't
  | Failure of 'e

Full name: The Digit type.WrappingType.Result<_,_>
union case Result.Ok: 't -> Result<'t,'e>
Multiple items
union case Result.Failure: 'e -> Result<'t,'e>

--------------------
active recognizer Failure: exn -> string option

Full name: Microsoft.FSharp.Core.Operators.( |Failure|_| )
val tryParse : input:string -> Result<Digit,string>

Full name: The Digit type.WrappingType.tryParse
val input : string
Int32.TryParse(s: string, result: byref<int>) : bool
Int32.TryParse(s: string, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
union case Result.Failure: 'e -> Result<'t,'e>
Multiple items
namespace System.Drawing

--------------------
type Drawing =
  | Square
  | Circle
  | Star

Full name: The Digit type.Drawings.Drawing
union case Drawing.Square: Drawing
union case Drawing.Circle: Drawing
union case Drawing.Star: Drawing
type Digit =
  | Zero
  | One
  | Two
  | Three
  | Four
  | Five
  | Six
  | Seven
  | Height
  | Nine

Full name: The Digit type.Digit
union case Digit.Zero: Digit
union case Digit.One: Digit
union case Digit.Two: Digit
union case Digit.Three: Digit
union case Digit.Four: Digit
union case Digit.Five: Digit
union case Digit.Six: Digit
union case Digit.Seven: Digit
union case Digit.Height: Digit
union case Digit.Nine: Digit
type Result<'t,'e> =
  | Ok of 't
  | Failure of 'e

Full name: The Digit type.Result<_,_>
val cases : Map<string,Digit>

Full name: The Digit type.cases
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 d : Digit
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
Multiple items
module Map

from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> =
  interface IEnumerable
  interface IComparable
  interface IEnumerable<KeyValuePair<'Key,'Value>>
  interface ICollection<KeyValuePair<'Key,'Value>>
  interface IDictionary<'Key,'Value>
  new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
  member Add : key:'Key * value:'Value -> Map<'Key,'Value>
  member ContainsKey : key:'Key -> bool
  override Equals : obj -> bool
  member Remove : key:'Key -> Map<'Key,'Value>
  ...

Full name: Microsoft.FSharp.Collections.Map<_,_>

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
val ofList : elements:('Key * 'T) list -> Map<'Key,'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Map.ofList
val tryParse : input:string -> Result<Digit,string>

Full name: The Digit type.tryParse
val tryFind : key:'Key -> table:Map<'Key,'T> -> 'T option (requires comparison)

Full name: Microsoft.FSharp.Collections.Map.tryFind
union case Option.Some: Value: 'T -> Option<'T>
val digit : Digit
union case Option.None: Option<'T>
val score : digit:Digit -> int

Full name: The Digit type.score
Fork me on GitHub