Can we really make illegal data unrepresentable?
I’m afraid we can’t, but let’s try
type TrimNonEmptyString internal (value : string) = member __.Value = value override __.ToString() = value override __.Equals(yobj) = match yobj with | 😕 TrimNonEmptyString as y -> (__.Value = y.Value) | _ -> false override __.GetHashCode() = hash value static member TryParse (value : string) = if System.String.IsNullOrWhiteSpace value then None else Some <| value.Trim() with interface System.IComparable with member __.CompareTo yobj = match yobj with | 😕 TrimNonEmptyString as y -> if __.Value > y.Value then 1 elif __.Value < y.Value then -1 else 0 | _ -> invalidArg "TrimNonEmptyString" "cannot compare values of different types" [<EntryPoint>] let main argv = System.Console.ReadLine() |> ignore; let test = TrimNonEmptyString (null) printfn "*%A*" test System.Console.ReadLine() |> ignore; 0
Here we go, a
NullReferenceException means we failed our goal.
Of course, I’m aware that it was an internal c.tor and there is the below easy fix, but that’s not my point.
let test = TrimNonEmptyString.TryParse null printfn "*%A*" test
Look at this
let test = TrimNonEmptyString.TryParse null if test.IsSome then printfn "*%s*" test.Value else printf "illegal state"
Again, it doesn’t prevent from a
NullReferenceException in case of mistyping
if not test.IsSome
Well, of course
.Value is unsafe and a better approach should be
match test with | Some(s) -> printfn "*%s*" s | None -> printf "illegal state"
Notice that the
NullReferenceException is due to a bug in F# 4.0, but – aside from this technical detail – we still have two ways to proceed.
type TrimNonEmptyString private (value : string) = member __.Value = value override __.ToString() = value
The first is to make the c.tor private, but that would lead us into a classical parser, the traditional check method in C#, again imho such is not a type truly making illegal data unrepresentable.
The other option is to make the c.tor available (I’d expect to be able to use the type as input for other functions) and add further checks
type TrimNonEmptyString (value : string) = member __.Value = if System.String.IsNullOrWhiteSpace value then "" else value override __.ToString() = if System.String.IsNullOrWhiteSpace value then "" else value
That’s what I eventually prefer, but we’re still far from a usable business case, at least we should see this type used as a constrained input to another function… Comments are welcomed.
I’ve found a nice use case from Domain Type options and converters.
Let’s define a trivial converter in our Domain example.
module Domain = let apply xo fo = match fo, xo with | Some f, Some x -> Some (f x) | _ -> None let (<*>) f x = apply x f let prodLength (s1 : string) (s2 : string) = s1.Length * s2.Length
and now the usage is shown in a couple of expecto tests
testCase "Prod lenght some some" <| fun () -> let s = Some prodLength <*> (TrimNonEmptyString.TryParse "asd") <*> (TrimNonEmptyString.TryParse "fsfs") Expect.isSome (s) "expected some" Expect.isTrue (s = Some (3 * 4)) "expected 12" testCase "Prod lenght none some" <| fun () -> let s = Some prodLength <*> (TrimNonEmptyString.TryParse null) <*> (TrimNonEmptyString.TryParse "fsfs") Expect.isNone (s) "expected none"
Back to classical C# programming. If an integer is your “legal” data, then the type
int makes illegal data unrepresentable. If you assign a string to it, the code doesn’t even compile. If you define an input parameter as
int, the caller can’t really pass anything else to the function.
This is not a static method parsing a string to check if it represents an integer.