F#, Programming, Software design

Power of composition with map and bind

In functional architecture functionalities get composed into workflows. Workflows are essential part of any business behavior modeling. Things get complicated when you need to build bigger systems from small components. Sometimes it is hard to find proper connectors to fit multiple functions having different inputs and outputs. There are various tools to achieve that composition in FP world which you could have heard by the names like functors, monoids or monads. These tools allow you to glue things together by connecting outputs of one functions to the inputs of another functions with proper transformations in between. In practice it is much easier to understand how it works than diving in category theory and trying to figure out the math beneath it.

🔌 Composition basics

When dealing with relatively simple types like strings and numbers connecting inputs and outputs is quite straightforward. Consider this example:

let addOne a = a + 1
let multipleByTwo a = a * 2

Here we defined two functions both of which takes a number as an input and returns number as an output, so their signatures are the same – we expect number on input and the result of operation in output is also a number:

(int -> int)

We can call it in following ways:

multipleByTwo (addOne 2)
// OR
2 |> addOne |> multiplyByTwo
val it : int = 6

We also can create new function which is composition of addOne and multiplyByTwo:

let addOneMultipliedByTwo = addOne >> multiplyByTwo
addOneMultipliedByTwo 2

This way you can build really complex logic from smaller pieces just like with a Lego bricks.

🅰️ ADTs are everywhere

More often, however, you will find yourself writing a bit more complicated things than adding or multiplying numbers. It could be custom types, or types based on other types which are known as algebraic data types (ADTs). It is very common to build up things from abstract types and provide functions which transform other values to that types. One very familiar to you example could be Maybe (a.k.a. Option) type which you could heard as Maybe monad or Maybe functor. In a nutshell it is a container for value or absence of the value. This is extremely effective abstraction to avoid nulls in your code and hence having peace 🧘 and no null reference exceptions everywhere.

In F# it is presented in Option module with set of functions to work with that type. So you have type Option<‘T> with possible value Value: ‘T or no value which is None. You can find tons of functions in the module. They help you building more complex things from smaller and make proper transformations for connecting functions which require that type.

Let’s have a quick look on how to use it:

let someValue = Some 10
let noValue = None

someValue |> Option.get // val it : int = 10
someValue |> Option.isSome // val it : bool = true
noValue |> Option.isNone // val it : bool = true
(10, someValue) ||> Option.contains // val it : bool = true
(99, someValue) ||> Option.defaultValue // val it : int = 10
(99, noValue) ||> Option.defaultValue // val it : int = 99

😲 When things go wrong

Now let’s have a small programming exercise. Suppose silly scenario where we have a players (any game you can imagine) and we need to check if the score player collected is good or not. So we come up with something like this:

type Player = { 
    Name: string 
    Score: int
}

let isGoodScore score = if score >= 70 then true else false

So all we need is to create players and check their scores:

let frank = { Name = "Frank"; Score = 90; }
let jack = { Name = "Jack"; Score = 37; }

frank.Score |> isGoodScore // val it : bool = true
jack.Score |> isGoodScore // val it : bool = false

“Hey, but player could have no score as well”

So how about to support that? Well, piece of a cake. Let’s make few minor changes:

type Player = { 
    Name: string 
    Score: int option
}
let frank = { Name = "Frank"; Score = Some 90; }
let john = { Name = "John"; Score = None; }
let jack = { Name = "Jack"; Score = Some 37; }

Nice! We’ve wrapped score in Option type just exactly like in requirement we got. How about isGoodScore function, will it still work?

frank.Score |> isGoodScore
error FS0001: Type mismatch. Expecting a
    'int option -> 'a'
but given a
    'int -> bool'
The type 'int option' does not match the type 'int'

Oops, we can’t use optional type with plain type like that:

So we need a ways to glue up monadic types like Option with functions working on plain values. And that’s where two most essential functions get into the big picture: map and bind.

🤝When composition meet ADT

As I mentioned before in the FP toolbox there various tools to help us with transformations. One such tool is map function. There are other names for it like fmap, lift, Select (think of C# LINQ). Each monadic-like type has this function.

Let’s have a look what signature of that function for Option:

(('a -> 'b) -> 'a option -> 'b option)

There 3 arguments: function which transforms input of type ‘a to ‘b, optional ‘a and optional ‘b. So how can we apply map for our use case? Pretty straightforward actually:

frank.Score |> Option.map isGoodScore // val it : bool option = Some true
john.Score |> Option.map isGoodScore // val it : bool option = None
jack.Score |> Option.map isGoodScore // val it : bool option = Some false

You see how return type is changed? We just applied standalone function which works on int to Option type. It lifted result of the function execution back to the Option. If input value is Some int it will be extracted from container (Option type), piped to the function and on the other end lifted up/wrapped back to the Option type. In case if there no value, it will just use None.

In C# IEnumerable with Select method on it works in the same way but applied to collections which means that collections are also ADTs. Here some visuals to help in understanding what’s going on:

👷 Bind it

Another very useful tool is bind function which you may have heard by other names like flatMap, collect, SelectMany. This one allows you to compose monadic functions in a little bit different way. Here the signature of the bind function for Option:

(('a -> 'b option) -> 'a option -> 'b option)

Let’s extend on our previous example and say that now we have an external source (database, file, etc.) from which we need to fetch players to find out score. So we define tryFindPlayer function as follows:

let tryFindPlayer name = 
    [ frank; john; jack ] |> List.tryFind (fun c -> c.Name = name)

List.tryFind is built-in function which returns Some ‘T if satisfies predicate in lambda or None. In our case it will return Some Player or None. Now we would be able to get the score of the player:

tryFindPlayer "Frank"
    |> Option.bind (fun c -> c.Score)

Here the visuals:

As you see, unlike map, bind allows you to compose up things within the same category (Option) but with different underlying types. It is flattening result, so instead of having Option<Option<int>> with bind it skips unnecessary wrapping.

💪The power of composition

There a lot of ADTs in form of data structures, workflows and other concepts which you need to combine to build working software: List<T>, Option<T>, State<T>, Async<T>, etc.

Once you get a grasp on how to use it – it becomes straightforward how to compose things up:

tryFindPlayer "Frank" 
    |> Option.bind (fun c -> c.Score)
    |> Option.map isGoodScore
val it : bool option = Some true

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s