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