Basics 17 - Option Pipeline with Map and Bind

Learning-FSharp/Ch17-OptionPipeline/Program.fs

Check out all the comments that are placed in the source file.

Pipeline

Imagine reading/writing a piece of code like this:

let result = input |> f1 |> f2 |> f3 |> f4

This is a pipeline. A pipeline (somewhat) guarantees:

  1. You are dealing with a sequential flow, one function call after the other, output of one goes to the other as input

  2. There is no branching or flow control involved; it's a straight arrow from start to finish

If you look at the pipeline from an imperative programming mindset, you may think:

  1. This is applicable only in very few cases... how does one solve large problems with a pipeline?

  2. What about branches/flow control etc.?

Complex, real-life problems can be solved with pipelines. The effort I am making in this article is to get you started.

Option Pipeline with Map and Bind

Let's build an example function:

  1. Your start with a number

  2. If the number is less than 1, return 0

  3. Add 1 to the number, number = number + 1

  4. If the number is odd, return 0, otherwise, return the number

static int NumberFunction(int input)
{
  if (input < 1)
    return 0;

  input++;

  if (input % 2 != 0)
    return 0;

  return input;
}

The above given is an imperative answer to the problem. It has branches and flow control.

Let's imagine that there is a sequential answer possible. It would look like this:

  1. Step 1: Input

  2. Step 2: Validation1 - can't be less than 1

  3. Step 3: Action1 - add 1

  4. Step 4: Validation2: can't be an odd number

  5. Step 5: Output

The real matter of importance in the above list are items no. 2, 3 and 4. Defining validation1, action1 and validation2.

Let's do it. Here we use int option type. Some case means there is result. None case means 0, no result.

// int -> int option
let validation1 num = if num < 1 then None else Some num

// int -> int 
let action1 num = num + 1

// int -> int option
let validation2 num = if num % 2 <> 0 then None else Some num

What's left now is creating a pipeline:

// int -> int
let numberFunction num =
    Some num // Step 1
    |> Option.bind validation1 // Step 2
    |> Option.map action1 // Step 3
    |> Option.bind validation2 // Step 4
    |> function // Step 5
        | Some(x) -> x
        | None -> 0

let result0 = numberFunction 0
printfn "%A" result0 // 0

let result1 = numberFunction 1
printfn "%A" result1 // 2

let result2 = numberFunction 2
printfn "%A" result2 // 0

If you don't want to write separate functions, here's a version with lambda expressions:

let numberFunction num =
    Some num
    |> Option.bind (fun num -> if num < 1 then None else Some num)
    |> Option.map (fun num -> num + 1)
    |> Option.bind (fun num -> if num % 2 <> 0 then None else Some num)
    |> function
        | Some(x) -> x
        | None -> 0

Revisiting Map and Bind

Let's revisit Option.map and Option.bind functions to understand how the above-given pipeline works.

Option.map: ('T -> 'U) -> 'T option -> 'U option

Option.bind: ('T -> 'U option) -> 'T option -> 'U option

We use map when the injected function's parameter and output both are regular data. We use bind when the injected function's parameter is regular data and the output is lifted data.

We created 3 functions that are used with Option.map/Option.bind:

// int -> int option
// Used with Option.bind
let validation1 num = if num < 1 then None else Some num

// int -> int
// Used with Option.map
let action1 num = num + 1

// int -> int option
// Used with Option.bind
let validation2 num = if num % 2 <> 0 then None else Some num

The key difference between Option.map and Option.bind, as illustrated in this example:

  1. Option.map is purely a transformation function

  2. Function action1 is passed to Option.map and all action1 does is return a new value

  3. Function validation1 and validation2 are not just transformation

  4. Both these functions, since return an int option, can change the trajectory

  5. Option.bind is used with validation1 and validation2. the best way to look at Option.bind is:

    1. It may or may not transform the data; may return the input itself; may transform the input and return

    2. It may cause trajectory change; Some or None in this case

Here is the C# version of the function again, which is slightly modified. This should give you a visual of how Option.map and Option.bind are used.

More Examples

Transformation Pipeline with Option.map

// A transformation pipeline
let transformationPipeline num =
    Some num
    |> Option.map (fun x -> x + 1) // add 1
    |> Option.map (fun x -> x * 2) // times 2
    |> Option.map (fun x -> x + 3) // add 3
    |> Option.map (fun x -> x * 4) // times 4
    |> function
        | Some(x) -> x
        | None -> 0

Validation Pipeline with Option.bind

// A validation pipeline
// Given number should be divisible by 2, 3, 4 and 5
let validationPipeline num =
    Some num
    |> Option.bind (fun x -> if x % 2 = 0 then Some x else None) // div by 2?
    |> Option.bind (fun x -> if x % 3 = 0 then Some x else None) // div by 3?
    |> Option.bind (fun x -> if x % 4 = 0 then Some x else None) // div by 4?
    |> Option.bind (fun x -> if x % 5 = 0 then Some x else None) // div by 5?
    |> function
        | Some(x) -> x
        | None -> 0

Concluding Remarks

  1. Pipelines are easier to read and write because of their linear/sequential nature

  2. Write your functions:

    1. Mapping functions for transforming data

    2. Binding functions for transforming data (optional) and changing the trajectory

  3. As we move forward we will visit some complex problem statements and see how those can be implemented in terms of pipelines

If you have reached so far, congratulations.

Keep reading!