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:
You are dealing with a sequential flow, one function call after the other, output of one goes to the other as input
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:
This is applicable only in very few cases... how does one solve large problems with a pipeline?
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:
Your start with a number
If the number is less than 1, return 0
Add 1 to the number, number = number + 1
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:
Step 1: Input
Step 2: Validation1 - can't be less than 1
Step 3: Action1 - add 1
Step 4: Validation2: can't be an odd number
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:
Option.map
is purely a transformation functionFunction
action1
is passed toOption.map
and allaction1
does is return a new valueFunction
validation1
andvalidation2
are not just transformationBoth these functions, since return an
int option
, can change the trajectoryOption.bind
is used withvalidation1
andvalidation2
. the best way to look atOption.bind
is:It may or may not transform the data; may return the input itself; may transform the input and return
It may cause trajectory change;
Some
orNone
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
Pipelines are easier to read and write because of their linear/sequential nature
Write your functions:
Mapping functions for transforming data
Binding functions for transforming data (optional) and changing the trajectory
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!