Basics 14 - Functional Patterns - Map and Bind
The Problem with Learning Functional Patterns
Patterns in the functional world are extremely different from patterns in the OOP world. It's a borderline impossible task to understand functional patterns for beginners. This happens for various reasons:
Functional programming has roots in mathematics and category theory. The lingo used is completely alien to a C#/Java programmer.
There are no classes/interfaces/inheritance etc. Just container types + functions.
Partial application makes matter worse. Here's an example:
This is the documentation for map
function from List
module. https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-listmodule.html.
The signature of List.map
is: mapping: ('T -> 'U) -> list: 'T list -> 'U list
This means it is a two-parameter function, which returns a list. So far so good. Then you search and study more, and realize, it's a cousin of LINQ's Select
method. No problems so far.
Then, since you are learning functional patterns, you study the map
function. You study category theory, and what the functional programming guides have to say. Loosely put, the description you find is:
map
returns a functionmap
lifts a function
At this point, nothing makes sense. What's the connection between List.map
, which is mapping: ('T -> 'U) -> list: 'T list -> 'U list
and returning/lifting a function? Are we dealing with List (and its elements) or with functions? Chaos!
The issue here is perspective, what one is looking at:
One perspective is very specific and involves both the parameters of
List.map
The other perspective, which is very abstract, involves only the first parameter of
List.map
No surprise, if you call
List.map
with the first parameter only, you get another function
Functional patterns are about abstract thinking. It's a slow process. You can't learn to think abstractly overnight. So, be patient.
How Functional Patterns are Evolved
Functional patterns are based on common recurring themes (problem statements, solutions) and giving them names. For instance:
Counting items in a
list
,set
,map
etc.Finding min, max, average etc.
Converting a list of numbers to another list where each value is doubled
Compressing a list of numbers to just one number (depending upon the use case)
The goal is:
You focus on what is needed
You don't focus on structural issues, for instance:
How to traverse a
list
or anarray
How to extract value out of an
option
or aResult
What's Lifting or Elevation?
Remember, in the world of functional programming you are dealing with two levels:
Regular data types:
int
,string
and so onHere you are dealing at the regular level
Nothing fancy about data or functions
Container data types:
option
,Result
,list
and so onHere you are dealing with data that's lifted, which means data is now inside a container; not dealing with an
int
, but aint list
or anint option
Here you have functions that operate on lifted data; a function that can operate on
int list
orint option
, notint
Lifting/elevating has two aspects:
Lifting/elevating/packaging/storing data into a container;
int
toint list
Lifting/elevating functions that operate on regular data types
A function that operates on
int
to operating onint list
orint option
This is where things get crazy confusing. If there is a function, which takes an
int
parameter, how can that be lifted? In other words, how can this function magically start operating on anint list
or anint option
Here's an example in C#. You are writing a program where you need to double int
values. So you write a function like this:
static int DoubleMyInt(int x)
{
return x * 2;
}
// Calling
int x = DoubleMyInt(whatever);
Now you need to double all the int
values in a list. So you write a function like this:
static int DoubleMyInt(int x)
{
return x * 2;
}
static List<int> DoubleMyIntList(List<int> list)
{
List<int> result = new();
foreach (int x in list)
{
result.Add(DoubleMyInt(x));
}
return result;
}
What we have now is a combo of:
The need (aka the business case) - double an
int
, already implemented asDoubleMyInt
Structural issues:
DoubleMyInt
can't operate onList<int>
, so we create another functionDoubleMyIntList
. This function has structural awareness, that is, how to create a new list, traverse an existing list etc.
Then you consult a smart friend of yours who suggests there is no need for DoubleMyIntList
function. You can simply write something like this:
var doubled = list.Select(DoubleMyInt).ToList();
Pay attention to what's happening here:
You have a function
DoubleMyInt
which operates on anint
valueSelect
method is enabling you to use the same function for operating onList<int>
In strict terms, this is not lifting DoubleMyInt
, but the next best thing, that is, there is a provision to use DoubleMyInt
with lifted data. You'd call it lifting DoubleMyInt
if you have a function called LiftMyFunctionToOperateOnIntList
. Something like this:
var liftedFunction = LiftMyFunctionToOperateOnIntList(DoubleMyInt);
var doubled = liftedFunction(someIntList);
Here liftedFunction
is a new function, which operates on List<int>
. This is lifting DoubleMyInt
and getting liftedFunction
.
Sadly, in C# there is no straightforward way to achieve this. It can be done, however, requires implementation (simulation) of partial application and whatnot.
Before we move forward, one important point to note here:
In regards to functions, operating on lifted data refers to parameters. For example:
int list -> int
,int list -> int list
,int option -> string
,int option -> ()
.The return type of function has nothing to do with the whole argument. These are examples of functions that are not operating on lifted data:
int -> int
,int -> string
,int -> int list
,int -> int option
.
Map
Very simply put, map
allows the transformation of lifted data.
If you have an int option
and you want to add 2 to its value:
First, you need to know if the
option
is inSome
orNone
case; forNone
case you exit withNone
onlyFor
Some
case:You need to extract the
int
value out ofint option
Add 2 to the value
Return a new
int option
with the new value
Something like this:
let add2 x = x + 2
let opt1 = Some 1
let opt2 = None
match opt1 with
| Some(x) -> Some(add2 x)
| None -> None
|> printfn "%A" // Some 3
match opt2 with
| Some(x) -> Some(add2 x)
| None -> None
|> printfn "%A" // None
All we need to do is add 2 to the lifted value, however, what we have is the function add2
, which is the business logic, plus a whole lot of structure-aware code. Not what we need.
Let's look at the Option.map
function. https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-optionmodule.html
Option.map
allows lifted value transformation using functions that don't deal with lifted data. The function add2
doesn't deal with lifted data. It deals with an int
only.
If you read the signature of Option.map
it says:
Give me a function with of type
'T -> 'U
, and a'T option
I will return
'U option
In the example below, function add2
is of type int -> int
, and we have an int option
.
let add2 x = x + 2
let opt1 = Some 1
let opt2 = None
Option.map add2 opt1 |> printfn "%A" // Some 3
Option.map add2 opt2 |> printfn "%A" // None
Here we focus on the function add2
only. Rest everything is handled by Option.map
. This is what I call abstraction. I will focus on what I need, and not on the structural issues.
Option.map
isn't the only map
function available. There are various container types for which map
function is available with similar ideas. Array.map
, List.map
, Seq.map
, Result.map
, Result.mapError
are some other examples, which we will discuss later.
Map as a Function Lifter
Let's try this:
let add2 x = x + 2
let result = Option.map add2
What do you think the result
is? What is its type?
Let's revisit the function signature of Option.map
: ('T -> 'U) -> 'T option -> 'U Option
Option.map add2
is a partial application of Option.map
, which means, result
should be a function of type int option -> int option
. In other words, result
is lifted add2
.
Here's the lifted function in action:
let add2 x = x + 2
let result = Option.map add2
let opt1 = Some 1
let opt2 = None
result opt1 |> printfn "%A" // Some 3
result opt2 |> printfn "%A" // None
Map and a Problem
Let's try this:
let add2IfEven x = if x % 2 = 0 then Some(x + 2) else None
let opt1 = Some 1
let result = Option.map add2IfEven opt1
Function add2IfEven
:
Adds 2 if the given number is even and returns
Some
If the number if not even returns
None
What do you think the type of result
is? It is int option option
, not int option
.
Let's revisit the function signature of Option.map
: ('T -> 'U) -> 'T option -> 'U Option
If the supplied function's (first parameter to Option.map
) return type is 'U
, Option.map
should return 'U option
. Option.map
lifts 'U
into 'U option
.
The function add2IfEven
is returning int option
itself. This means you'd get int option option
.
In other words: if the function injected for map
returns lifted data, you will end up with double lifting. map
function (regardless of the type of container) is designed for injected functions that operate on and return regular data.
So, what do you do when you need to use add2IfEven
? The answer is: use map
function's cousin named bind
.
Bind
The bind function has two aspects. One, I will not cover at this moment as it is related to continuations, computation expressions, monadic bind etc. For the other one, let's look at the Option.bind
function. https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-optionmodule.html
If you read the signature of Option.bind
it says:
Give me a function with of type
'T -> 'U option
, and a'T option
I will give
'U option
The only difference between the signatures of Option.map
and Option.bind
is the first parameter, the type of function.
Option.map
asks for 'T -> 'U
Option.bind
asks for 'T -> 'U option
As I mentioned in the previous section, Option.map
lifts 'U
into 'U option
. Option.bind
on the other side, expects 'U option
to be returned by the injected function.
let add2IfEven x = if x % 2 = 0 then Some(x + 2) else None
let opt1 = Some 1
let opt2 = Some 2
let opt3 = None
Option.bind add2IfEven opt1 |> printfn "%A" // None
Option.bind add2IfEven opt2 |> printfn "%A" // SOme 4
Option.bind add2IfEven opt3 |> printfn "%A" // None
Bind as a Function Lifter
Same as Option.map
, Option.bind
can lift a function.
add2IfEven x = if x % 2 = 0 then Some(x + 2) else None
let result = Option.bind add2IfEven
let opt1 = Some 1
let opt2 = Some 2
let opt3 = None
result opt1 |> printfn "%A" // None
result opt2 |> printfn "%A" // SOme 4
result opt3 |> printfn "%A" // None
Summary
Map:
Allows you to transform data that's lifted
Simply write your functions (business logic) where you transform data
Both the input and output of the function (business logic) should be regular data
You can also use
map
as a function lifter
Bind
Similar to
map
, except that the function (business logic) should return lifted dataYou can also use bind as a function lifter
If you have reached so far, congratulations.
Keep reading!