Matthew MacFarland
Matthew MacFarland

Reputation: 2731

How can I add to a type that contains an integer inside a Result of a discriminated union?

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?

Update 1 Trying Result.bind

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

Answers (1)

Brian Berns
Brian Berns

Reputation: 17038

Suggestion 1

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

Suggestion 2

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

Suggestion 3

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

Related Questions