Functional argument checking

Using the railway pattern for validating function call arguments in F#

Functional argument checking

Defensive programming teaches us never to trust input data; this includes function call arguments, especially if the function performs sensitive business logic. In a procedural language, e.g., C#, this is usually implemented with successive value checks that trigger an early return if the check fails, something along these lines:

void Transfer(Transference t)
{
    if (String.IsNullOrWhiteSpace(t.SourceAccount))
    {
        Console.WriteLine("The source account cannot be null or empty");
        return;
    }
    if (String.IsNullOrWhiteSpace(t.TargetAccount))
    {
        Console.WriteLine("The target account cannot be null or empty");
        return;
    }
    if (t.Amount <= 0)
    {
        Console.WriteLine($"The transference amount must be greater than zero, {t.Amount} was asked");
        return;
    }

    Console.WriteLine($"Execute the transference logic for {t}");
}

Alas, in a functional language, e.g., F#, we don't have early returns as if-then-else is not a statement but an expression; furthermore, if-then doesn't exist because what would be the value of such expression when the condition is false? So, to check several values, we have to do something like this:

let transfer (t: Transference) =
    if String.IsNullOrWhiteSpace t.SourceAccount then
        printfn "The source account cannot be null or empty"
    elif String.IsNullOrWhiteSpace t.TargetAccount then
        printfn "The target account cannot be null or empty"
    elif t.Amount <= 0M then
        printfn "The transference amount must be greater than zero, %f was asked" t.Amount
    else
        printfn "Execute the transference logic for %A" t

Even though we need fewer lines of code in F# than in C#, your mind is forced to follow through the if-elif-else cascade to the very end, whereas in C# you can reset your mental stack at each return; moreover, in real-life applications, you will probably have to do several more validations, making the whole thing harder to read and understand.

Luckily we can seize on functional ideas to solve the problem with something like this:

let transfer (t: Transference) =
    t
    |> checkSourceAccount
    |> bind checkTargetAccount
    |> bind checkAmount
    |> bind doTransfer
    |> printfn "%A"

This code says:

  1. Take the t argument
  2. Apply the checkSourceAccount function to t
  3. Apply the checkTargetAccount function to the result of the previous step
  4. Apply the checkAmount function to the result of the previous step
  5. Apply the doTransfer function to the result of the previous step
  6. Finally, print the result of the last executed call

Why is bind there? What does it do? Something fundamental, it checks the result of the previous step: if it is Ok then it just goes on with the next call, but if it is Error then it bypasses the next call. Effectively, this function calls pipeline short-circuits as soon as an error is triggered, the early return on the first failure is achieved by bind. Do note that in the final step we say print the result of the last executed call. When the pipeline short-circuits, the last executed call may be the first, the second, or any other on the pipeline, not necessarily the last one.

How do the validation functions notify whether their check passed or failed? Let's see the code for the first one:

let checkSourceAccount (t: Transference) =
    if String.IsNullOrWhiteSpace t.SourceAccount then
        Error "The source account cannot be null or empty"
    else
        Ok t

This function returns something of type Error, signaling a failed validation, or something of type Ok, signaling a passed validation. In the case of a failed validation, it also returns a string describing the problem; in the case of a passed validation it must return the original argument; this is important and mandatory because this argument will be passed further down the pipeline. Of course, the validation could be far more sophisticated, but the pattern is simple enough: Error "message" if something goes wrong, Ok originalArgument if everything is fine in this step. If any validation step becomes too complex, break it into two steps, er, functions, and extend the pipeline. Modular, simple to understand, efficient, what's not to like about it?

Just for completion, here is the code for the remaining functions of the pipeline:

let checkTargetAccount (t: Transference) =
    if String.IsNullOrWhiteSpace t.TargetAccount then
        Error "The target account cannot be null or empty"
    else
        Ok t

let checkAmount (t: Transference) =
    if t.Amount <= 0M then
        Error $"The transference amount must be greater than zero, {t.Amount} was asked"
    else
        Ok t

let doTransfer (t: Transference) =
    try
        printfn "Execute the transference logic"
        Ok t
    with ex ->
        Error ex.Message

Nothing new, except maybe that the doTransfer function most probably would call external resources, e.g., a Web service or a database, which can trigger exceptions,  so we use a familiar try-catch. Of course, if an exception does happen, we should have to add logic for logging the problem and mapping the external error to some adequate business error. By the way, I beg you not to use exceptions for business logic.

Finally, as lambdas are so popular now, let's see how to use them in our validation pipeline:

let transfer' (t: Transference) =
    t
    |> fun t -> if String.IsNullOrWhiteSpace t.SourceAccount then Error "The source account cannot be null or empty" else Ok t
    |> bind (fun t -> if String.IsNullOrWhiteSpace t.TargetAccount then Error "The target account cannot be null or empty" else Ok t)
    |> bind (fun t -> if t.Amount <= 0M then Error $"The transference amount must be greater than zero, {t.Amount} was asked" else Ok t)
    |> bind doTransfer
    |> printfn "%A"

This code doesn't escalate well as validations become more complex, but a healthy combination of predefined functions and lambdas may well be your best option.

This post shows one example of railway-oriented programming, a functional pattern so baptized by Scott Wlaschin (tongue-in-cheek as he knows this is not a programming paradigm, just a helpful technique), he explains the design in detail in his post Railway Oriented Programming, do check it out! I also invite you to check Kit Eason's book Stylish F# 6, 2nd Ed., in there he dedicates all of Chapter 11 to railway-oriented programming. All I wanted to do here is show you a simple use of the pattern and pique your curiosity to go and read those more authoritative resources.