Thoughts On Input Validation Pattern From A Noob

This is very subjective, but I’d like to get some thoughts on this before I put this into practice. On the one hand, I’m proud I got this to compile…but is it too convoluted?

I’m trying to validate user input and:

  • retain valid / remove invalid values from input
  • print a list of all validation errors

Looking at other validation patterns like using applicatives, they only give me the error list.

type Input = 
    {name : string option; dob : DateTime option; address : string option}
type InputValid = {name : string; dob : DateTime; address : string}

let errorFun args errors =
    printfn "%O" args //refresh form with only valid args
    errors |> List.iter (fun s -> printfn "%s" s) //print errors up top

let successFun argsV =
    printfn "%O" argsV //do stuff

The idea is to have a validation function that takes the input and returns either an “InputValid” record or the valid part of “Input” * the invalid part in “string list”.

And this is what I came up with:

let inline bindResult f acc =
    match acc with
    | Ok (args, successFun) ->
        match f args with
        | Ok param -> Ok (args, (successFun param))
        | Error (args, ex) -> Error (args, ex)
    | Error (args, ex) ->
        match f args with
        | Ok _ -> Error (args, ex)
        | Error (args, ex') ->
            Error (args, (List.concat [ex'; ex]))

let validateName (args: Input) =
    match args.name with
    | Some n when n.Length > 3 -> Ok n
    | Some _ -> Error ( {args with name = None}, ["no bob and toms allowed"])
    | None -> Error (args, ["name is required"])

let validateDob (args: Input) =
    match args.dob with
    | Some dob when dob > DateTime.Now.AddYears(-12) -> Ok dob
    | Some _ -> Error ({args with dob = None}, ["get off my lawn"])
    | None ->  Error (args, ["dob is required"])

let validateAddress (args: Input) =
    match args.address with
    | Some a -> a |>  Ok
    | None -> Error (args, ["add1 is required"])

let validate (args : Input) =
    let createValid n d a =
        {InputValid.name = n; dob = d ; address = a}
    Ok (args, createValid)
    |> bindResult validateName
    |> bindResult validateDob
    |> bindResult validateAddress
    |> function
    | Ok (_, valid) -> successFun valid
    | Error (args, ex) -> errorFun args ex

This is a really interesting problem - thank you for sharing :+1:

The answer is still applicatives, but it’s a bit more involved. I’ve just spent some hours this weekend putting an elegant prototype together.

Quite a bit of work went into it, so I’m going to turn it a series of blog posts. I’m afraid I’ll have to schedule them for end of December; I hope that you don’t mind the wait…

2 Likes

I started using this pattern and it’s working well. I can’t really wait because this is for Level1Tech’s Devember:

Discord Bot In F# - Community Blog - Level1Techs Forums

I’ve been poking F# for a few years and used Devember as an excuse to give it a real go.

I did refine it a slight bit. The pre-validated args contain an errors collection now. When a parameter is invalid, the field is set to none and becomes appended to the errors list.


 let inline bindResult f acc =
    match acc with
    | Ok (args, successFun) ->
        match f args with
        | Ok param -> Ok (args, (successFun param))
        | Error args' -> Error args'
    | Error args ->
        match f args with
        | Ok _ -> Error args
        | Error args' -> Error args'

  let (|>>) y x = bindResult x y

//--------------

  let private validateCategory (args: qBotArgs) =
    match args.LobbyCat with
    | Some sCat ->
        match getCategoryByName args.Goo.Guild sCat with
        | Some oCat -> Ok oCat
        | None -> Error ({args with LobbyCat = None; Errors = ("Invalid Category Name: " + sCat) :: args.Errors})
    | None -> Error ({args with LobbyCat = None; Errors = "Category Is Required" :: args.Errors})

  let private validate (args: qBotArgs) =
    Ok (args, (qBotValid.create args.Server args.Goo))
    |>> validateAdmins
    |>> validateCaptains
    |>> validateCategory
    |> function
    | Ok (_, argsV) -> Ok (argsV)
    | Error ex -> Error ex

I know that it’s now ‘too late’, but for anyone interested, here’s my principled take on the problem: https://blog.ploeh.dk/2020/12/28/an-f-demo-of-validation-with-partial-data-round-trip

1 Like

Thanks for keeping "no bob and toms allowed" in there :slight_smile:

I’m still digesting this. I don’t understand how this part works:

    validation {
        let! name = validateName args
        and! dob = validateDoB now args
        and! address = validateAddress args
        return { Name = name; DoB = dob; Address = address }
    }
    |> Result.mapError (fun (f, msgs) -> f args, msgs)

But I like that it’s more flexible. You were able to pass now into validateDoB. Mine forced all the validation methods to use the same sig, but that was an issue in a couple of places. I had an issue where a couple of validation functions needed a single expensive lookup. There was no good way to share that value between them.

The computation expression, or the subsequent map?

The computation expression uses the new support for applicative functors that F# now has. It’s just syntactic sugar over the underlying functions that the computation builder exposes. It can be a good exercise to work through how to desugar such expressions.

Yes, I have yet to get my hands dirty with computational expressions, so that is the majority of it.

The other part is in the merge:

  1. Ok xres, Ok yres -> Ok (xres, yres)
    It looks like the signature after 4 successful validation functions would be ('a * ('b * ('c * 'd))). Is that correct? If so it’s just a matter of understanding how the comp exp unwraps the tuples of tuples into the field names before the record is instantiated.

    But if it’s only merging results in case of an Error(s), why return anything?

    | Ok _, Ok _ -> Ok () 
  1. Error (f, e1s), Error (g, e2s) -> Error (f >> g, e2s @ e1s)
    From a noobs perspective, the use of the id function made this really difficult to understand. I couldn’t find where you defined id. By time I realized it was short hand for (fun x -> x), I was too far removed from the code demo that it confused me even more. One extra aside would have saved me a few hours:
    // When a property is already blank, the "merge" accumulator just 
    // needs a method to pass the Input unchanged:
    // (fun (args: Input) -> args)  
    | None -> Error ((fun (args: Input) -> args), ["required"])
    // Or, we can use the F#'s identity method for this:
    | None -> Error (id, ["required"] // (let id (x: 'A) : 'A = x)

Your guess about the nested tuple may be correct, but I don’t know. The documentation still hasn’t been updated, so I don’t know exactly how MergeSources desugars. I’d imagine that it might instead propagate the values in curried form, but that’s just how it works if you define the applicative instances yourself.

But if it’s only merging results in case of an Error(s), why return anything?

Isn’t not only merging errors. The patterns you just asked about merges Ok values.