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: |
|
It is quite flacky modeling. What would this represent ?
1: 2: 3: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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: |
|
We can the try:
1: 2: |
|
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: |
|
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.
| Red
| Green
| Blue
| Yellow
Full name: The Digit type.Color
Full name: The Digit type.WithIntegers.Digit
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<_>
Full name: The Digit type.WithIntegers.Card
union case Card.Digit: Digit * Color -> Card
--------------------
type Digit = int
Full name: The Digit type.WithIntegers.Digit
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
union case Digit.Digit: int -> Digit
--------------------
type Digit = private | Digit of int
Full name: The Digit type.WrappingType.Digit
Full name: The Digit type.WrappingType.digit
Full name: Microsoft.FSharp.Core.Operators.invalidArg
| Ok of 't
| Failure of 'e
Full name: The Digit type.WrappingType.Result<_,_>
union case Result.Failure: 'e -> Result<'t,'e>
--------------------
active recognizer Failure: exn -> string option
Full name: Microsoft.FSharp.Core.Operators.( |Failure|_| )
Full name: The Digit type.WrappingType.tryParse
Int32.TryParse(s: string, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
namespace System.Drawing
--------------------
type Drawing =
| Square
| Circle
| Star
Full name: The Digit type.Drawings.Drawing
| Zero
| One
| Two
| Three
| Four
| Five
| Six
| Seven
| Height
| Nine
Full name: The Digit type.Digit
| Ok of 't
| Failure of 'e
Full name: The Digit type.Result<_,_>
Full name: The Digit type.cases
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<_>
Full name: Microsoft.FSharp.Collections.List.map
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
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>
Full name: Microsoft.FSharp.Collections.Map.ofList
Full name: The Digit type.tryParse
Full name: Microsoft.FSharp.Collections.Map.tryFind
Full name: The Digit type.score