Reputation: 2731
I'm working on a single case discriminated union with a module to create instances of the type and return a Result of either Ok if the input was valid or Error otherwise. Here is what I have so far.
type ErrorMessage = string
type NonNegativeInt = private NonNegativeInt of int
module NonNegativeInt =
let create (inputInt:int) : Result<NonNegativeInt, ErrorMessage> =
if inputInt >= 0 then
Ok (NonNegativeInt inputInt)
else
Error ("inputInt must be >= 0")
let value (NonNegativeInt intVal) = intVal
I would like to add an integer to an instance of this type using the create function so it will block negatives. I've got the first test working this way.
[<Fact>]
member this.``NonNegativeInt test`` () =
let nonNegativeResult = NonNegativeInt.create 5
let newNonNegativeResult = match nonNegativeResult with
| Ok result ->
let intVal = NonNegativeInt.value result
let newIntVal = intVal + 1
NonNegativeInt.create newIntVal
| Error _ ->
nonNegativeResult
match newNonNegativeResult with
| Ok result ->
Assert.Equal(6, NonNegativeInt.value result)
| Error _ ->
Assert.Fail("Error creating new NonNegativeInt")
This is pretty much unusable this way. Is there a more concise way to accomplish this task without all the unwrapping, wrapping, and pattern matching? Is Result.bind the way to go?
This is a better, but still feels a bit clumsy. Maybe the NonNegativeInt
module needs another function besides create and value to make this easier.
[<Fact>]
member this.``NonNegativeInt test2`` () =
let nni1 = NonNegativeInt.create 5
let nni2 = nni1
|> Result.bind (fun x -> NonNegativeInt.create ((NonNegativeInt.value x) + 1))
let expectedResult = NonNegativeInt.create 6
Assert.Equal(expectedResult, nni2)
Upvotes: 0
Views: 73
Reputation: 17038
You could use a computation builder to make the code cleaner:
type ResultBuilder() =
member _.Return(x) = Ok x
member _.ReturnFrom(res : Result<_, _>) = res
member _.Bind(res, f) = Result.bind f res
let result = ResultBuilder()
Then your example becomes:
let test () =
result {
let! nni = NonNegativeInt.create 5
return! NonNegativeInt.create (nni.Value + 1)
}
test () |> printfn "%A" // Ok NonNegativeInt 6
I also added a member to make it easier to access a NonNegativeInteger's value:
type NonNegativeInt =
private NonNegativeInt of int
with
member this.Value =
let (NonNegativeInt n) = this in n
Having an NNI type and then wrapping it in a Result is like wearing both a belt and suspenders. To simplify things further, you could get rid of the NNI type entirely, and just keep the validation logic:
module NonNegativeInt =
let create (inputInt:int) : Result<int, ErrorMessage> =
if inputInt >= 0 then
Ok inputInt
else
Error ("inputInt must be >= 0")
let test () =
result {
let! n = NonNegativeInt.create 5
return! NonNegativeInt.create (n + 1)
}
test () |> printfn "%A" // Ok 6
Alternatively, you could keep the NNI type and trust the caller to use it with valid values (without wrapping in a Result). This is what FsCheck does, for example:
///Represents an int >= 0
type NonNegativeInt = NonNegativeInt of int with
member x.Get = match x with NonNegativeInt r -> r
override x.ToString() = x.Get.ToString()
static member op_Explicit(NonNegativeInt i) = i
Upvotes: 1