1
\$\begingroup\$

I am still new to functional programming and found myself trying to filter and map a list of discriminating unions. I could be wrong, but it started to feel very unnatural to do this. I just couldn't figure out how else to get the workflow that I wanted in the CreateThing method.

The IsTrue/False and Value/Error methods are very useful, but their implementations seem awkward. But they are what allows me to work with DUs in list form.

Is there a more elegant/natural solution to this?

module Test 

open System.Collections.Generic

// some borrowed finaglery from s.o.
module Seq =
    // borrowed from: https://stackoverflow.com/a/12564899
    // takes until the condition is true plus one more
    let takeUntil predicate (s:seq<_>) = 
        let rec loop (en:IEnumerator<_>) = seq {
            if en.MoveNext() then
                yield en.Current
            if predicate en.Current then
                yield! loop en 
            }

        seq { use en = s.GetEnumerator()
            yield! loop en 
            }

// the union in question
type Valid<'a> = 
    | Valid of 'a 
    | Invalid of string

// the inspectors and destructors in question
module Valid =

    let IsTrue v = match v with |Valid _-> true |_-> false
    let IsFalse v = match v with |Valid _-> false |_-> true
    let Value v = match v with |Valid v -> v | Invalid _ -> failwith "no value"
    let Error v = match v with |Valid _ -> failwith "no error" | Invalid i -> i

module UseCase = 

    type Thing = {
        Values : int list
    }

    let ValidateNumber (i : int) : Valid<int> = 
        if i = 3 then Invalid "no threes yo" else Valid i

    let CreateThing (inputs : int seq) : Valid<Thing> =
        // pull items through the seq until i hit a problem
        let worked = inputs |> Seq.map ValidateNumber |> Seq.takeUntil Valid.IsFalse |> Seq.toList

        // see if it all worked out or not
        let success = List.last worked |> Valid.IsTrue

        if success then
            // deconstruct my 'valids' back to primitive ints
            Valid {Thing.Values = worked |> List.map Valid.Value}
        else
            // deconstruct my last error into a new error
            Invalid (List.last worked |> Valid.Error)
\$\endgroup\$
2
\$\begingroup\$

Fundamentally, for working with DUs, there is no other way. A DU represents values that can be "either this or that". This means that, when you're looking at one of these values, your code has to handle all possible options of what that value can be, otherwise your program will simply not know what to do for certain cases at runtime. There is no way around it.

That said, you can express this basic idea in different ways.

First, you can use the function keyword to shorten the pattern match a bit. let f = function is just a shorthand for let f x = match x with:

let IsTrue = function Valid _ -> true | _ -> false

Second, you can express IsFalse as a negation of IsTrue:

let IsFalse = not << IsTrue

Alternatively, you could extract the pattern matching part into a function (commonly referred to as "catamorphism"):

let inline validCata ifValue ifError = function Value x -> ifValue x | Error x -> ifError x

and then express the other functions in terms of it:

let inline IsTrue x = validCata (ct true) (ct false) x
let inline IsFalse x = validCata (ct false) (ct true) x
let inline Value x = validCata id (fun _ -> failwith "") x
let inline Error x = validCata (fun _ -> failwith "") id x

(where ct is a trivial constant function, which is sadly missing in F# standard library)

As for the CreateThing function, let me see if I understood the intent correctly: you're trying to validate the sequence of numbers and bail on the first error, returning it; or return the list of numbers if all of them are valid.

If this is the correct intent, you don't need to wrap each number in a Valid at all. Just run the validation function on them until it returns an error, and if it doesn't, return the original list. To run a function on a sequence and stop when it hits the first time, there is a handy library function Seq.tryPick:

let errorFor i =
    if i = 3 then Some "no threes yo" else None

let createThing inputs = 
    let firstError = Seq.tryPick errorFor inputs
    match firstError with
    | Some err -> Invalid err
    | None -> Valid { Thing.Values = List.ofSeq inputs }

If you hit this sort of problem often, and really want to use Valid as a general-purpose mechanism for dealing with errors, you can extract the "run sequence until error" part as a reusable thing:

let validSeq (s: Valid<'a> seq) : Valid<'a list> =
    let getError = function Valid _ -> None | Error err -> Some err
    let getValue = function Valid x -> x | Error _ -> Unchecked.defaultOf<'a>
    let lst = List.ofSeq s
    match List.tryPick getError lst with
    | Some err -> Error err
    | None -> lst |> List.map getValue |> Valid

let validMap f = function
    | Value v -> Valid (f v)
    | Error err -> Error err

let createThing inputs = 
    inputs
    |> Seq.map ValidateNumber 
    |> validSeq 
    |> validMap (fun vs -> { Thing.Values = vs })

(note that here I have also extracted validMap as a reusable thing)

Final note: unless you have actually hit (and measured) performance issues, don't work with sequences, work with lists instead. Since F# is not pure, running sequences multiple times can be expensive and unpredictable.

\$\endgroup\$
  • \$\begingroup\$ An incredibly thoughtful response to start my morning with. You've given me a lot to digest here. Thank you so much for your time! \$\endgroup\$ – TBone Feb 20 '18 at 14:15

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.