πFunctional patterns
Introduction
Origins
The patterns studied here are Monoid, Functor, Applicative, Monad. All these patterns are related, but we will only study the relationship between Functor, Applicative and Monad:
There are more functional patterns coming from category theory, but they are out of scope.
Fβ― hidden patterns
Fβ― uses functional patterns under the hood:
OptionandResultare monadic typesAsyncis monadicCollection types
Array,ListandSeqare monadic types too!Computation expressions can be monadic or applicative or monoidal
Fβ― is designed so that developers don't need to know these patterns to code in Fβ―, including the use of computation expressions (CEs). However, in my opinion, it is preferable to have a basic understanding of these patterns in order to design computation expressions.
General definition
In Fβ―, the functional patterns consist of:
A type
1 or 2 operations on this type
An eventual special instance of this type
Some laws constraining/shaping the whole
The type is generally noted M<'T>, where M is a generic type and 'T its type parameter referring to the type of elements that can be contained in M.
Monoid
Etymology (Greek): monos (single, unique) β’ eidos (form, appearance)
β Type T defining a set with:
Binary operation
+:T -> T -> Tβ To combine 2 elements into 1Neutral element
e(a.k.a. identity)
Monoid laws
1. Associativity
+ is associative
β a + (b + c) β‘ (a + b) + c
2. Identity Element
e is combinable with any instance a of T without effect
β a + e β‘ e + a β‘ a
Monoid examples
int
+ (add)
0
i + 0 = 0 + i = i
int
* (multiply)
1
i * 1 = 1 * i = i
string
+ (concat)
"" (empty string)
s + "" = "" + s = s
'a list
@ (List.append)
[] (empty list)
l @ [] = [] @ l = l
Functions
>> (compose)
id (fun x -> x)
f >> id = id >> f = f
Functor
Functor definition
β Any generic type, noted F<'T>, with a map operation:
Signature:
map: (f: 'T -> 'U) -> F<'T> -> F<'U>
map preserves the structure: e.g. mapping a List returns another List.
Functor laws
Law 1 - Identity law
Mapping the id function over a Functor F should not change F.
β map id F β‘ F
Law 2 - Composition law
Mapping the composition of 2 functions f and g is the same as
mapping f and then mapping g over the result.
β map (f >> g) β‘ map f >> map g
Functor examples
Option<'T>
Option.map
Result<'T, _>
Result.map
List<'T>
List.map
Array<'T>
Array.map
Seq<'T>
Seq.map
Async<'T> too, but through the async CE π
Monad
Monad definition
β Any generic type, noted M<'T>, with:
Construction function
returnSignature :
(value: 'T) -> M<'T>β Wrap (lift/elevate) a value
Chaining function
bindNoted
>>=(>β>β=) as an infix operatorSignature :
(f: 'T -> M<'U>) -> M<'T> -> M<'U>Take a monadic function
fCall it with the eventual wrapped value(s)
Get back a new monadic instance of this type
Monad laws
1. Left Identity
return then bind are neutral.
β return >> bind f β‘ f
2. Right Identity
bind return is neutral, equivalent to the id function:
β m |> bind return β‘ m |> id β‘ m
βοΈ It's possible because return has the signature of a monadic function.
3. Associativity
bind is associative.
Given two monadic functions f: 'a -> M<'b> and g: 'b -> M<'c>
β (m |> bind f) |> bind g β‘ m |> bind (f >> bind g)
π‘ bind allows us to chain monadic functions, like the |> for regular functions
Monad examples
Option<'T>
Option.bind
Some
Result<'T, _>
Result.bind
Ok
List<'T>
List.collect
List.singleton
Array<'T>
Array.collect
Array.singleton
Seq<'T>
Seq.collect
Seq.singleton
Async<'T> too, but through the async CE π
Monad vs Functor
A monad is also a functor
mapcan be expressed in terms ofbindandreturn:map fβ‘bind (f >> return)
βοΈ Note: Contrary to the monad with its return operation, the functor concept does not need a "constructor" operation.
Monad alternative definition
A monad can be defined with the flatten operation instead of the bind
β Signature: M<M<'T>> -> M<'T>
Then, the bind function can be expressed in terms of map and flatten:
β bind β‘ map >> flatten
π‘ This is why bind is also called flatMap.
Regular functions vs monadic functions
Pipeline
Regular
β· pipe
(f: 'a -> 'b) -> (x: 'a) -> 'b
Monadic
>>= bind
(f: 'a -> M<'b>) -> (x: M<'a>) -> M<'b>
Composition
Regular
>> comp.
(f: 'a -> 'b) -> (g: 'b -> 'c) -> ('a -> 'c)
Monadic
>=> fish
(f: 'a -> M<'b>) -> (g: 'b -> M<'c>) -> ('a -> M<'c>)
Fish operator definition:
let (>=>) f g = fun x -> f x |> bind gβ‘f >> (bind g)Composition of monadic functions is called Kleisli composition
Monads vs Effects
Effect (a.k.a. "side effect"): β change somewhere, inside the program (state) or outside β examples:
I/O (Input/Output): file read, console write, logging, network requests
State Management: global variable update, database/table/row delete
Exceptions/Errors: program crash
Non-determinism: same input β β value: random number, current time
Concurrency/Parallelism: thread spawn, shared memory
Pure function causes no side effects β deterministic, predictable β FP challenge: separate pure/impure code (separation of concerns)
Monads purposes:
Encapsulate and sequence computations that involve effects,
Maintain purity of the surrounding functional code,
Provide a controlled environment in which effects can happen.
Dealing with a computation that has an effect using monads means:
Wrapping: we don't get a value directly, we get a monadic value that represents the computation and its associated effect.
Sequencing:
bind(orlet!in a monadic CE) allows you to chain together effectful computations in a sequential order.Returning:
returnwraps a pure value β computation w/o effects. π A monadic sequence can mix pure and effectful computations.
From the caller perspective, a function returning a monadic value is pure. β Encapsulated effects only "happen" when monadic value is evaluated.
Examples in Fβ―:
Async: by callingAsync.RunSynchronously/StartOption/Result: by pattern matching and handle all casesSeq: by iterating the delayed sequence of elements
π Monads effectively bridge the gap between:
mathematical elegance of pure functional programming
practical necessity of interacting with an impure, stateful world
Other common monads
βοΈ Rarely used in Fβ―, but common in Haskell
Reader: to access a read-only environment (like configuration) throughout a computation without explicitly passing it around
Writer: accumulates monoidal values (like logs) alongside a computation's primary result
State: manages a state that can be read and updated during a computation
IO: handles I/O effects (disk, network calls...)
Free: to build series of instructions, separated from their execution (interpretation phase)
Applicative (Functor)
Applicative definition
β Any generic type, noted F<'T>, with:
Construction function
pure(β‘ monad'sreturn)Signature :
(value: 'T) -> F<'T>
Application function
applyNoted
<*>(same*than in tuple types)Signature :
(f: F<'T -> 'U>) -> F<'T> -> F<'U>Similar to functor's
map, but where the mapping function'T -> 'Uis wrapped in the applicative object
Applicative laws
There are 4 laws:
Identity and Homomorphism relatively easy to grasp
Interchange and Composition more tricky
Law 1 - Identity
Same as the functor identity law applied to applicative:
Functor
map id F β‘ F
Applicative
apply (pure id) F β‘ F
Law 2 - Homomorphism
π‘ Homomorphism means a transformation that preserves the structure.
β pure does not change the nature of values and functions so that we can apply the function to the value(s) either before or after being wrapped.
(pure f) <*> (pure x) β‘ pure (f x) apply (pure f) (pure x) β‘ pure (f x)
Law 3 - Interchange
We can provide the wrapped function Ff first or the value x, wrapped directly or captured in (|>) x (partial application of the |> operator used as function)
Ff <*> (pure x) β‘ pure ((|>) x) <*> Ff
π‘ When Ff = pure f, we can verify this law with the homomorphism law:
Law 4 - Composition
Cornerstone law: ensures that function composition works as expected within the applicative context.
Hardest law, involving to wrap the
<<operator (right-to-left compose)!
Ff <*> (Fg <*> Fx) β‘ (pure (<<) <*> Ff <*> Fg) <*> Fx
π‘ Same verification:
Applicative vs Functor
Every applicative is a functor
β We can define map with pure and apply:
map f x β‘ apply (pure f) x
π‘ It was implied by the 2 identity laws.
Applicative vs Monad
Every monad is also an applicative
pureandreturnare just synonymapplycan be defined usingbindgiven
mxa wrapped valueM<'a>and
mfa wrapped functionM<'a -> 'b>apply mf mxβ‘mf |> bind (fun f -> mx |> bind (fun x -> return (f x)))
apply vs bind π‘
Where
applyunwraps bothfandx, 2 nestedbinds are required.bindextra power comes from its ability to let its first parameter β the function'a -> M<'b>β create a whole new computational path.
Applicative: multi-param curried function
Applicative helps to apply to a function its arguments (e.g. f: 'x -> 'y -> 'res) when they are each wrapped (e.g. in an Option).
Let's try by hand:
π‘ We can recognize the Option.map2 function.
π€ Is there a way to handle any number of parameters?
The solution is to use apply N times, for each of the N arguments, first wrapping the function using pure:
Alternative syntax:
Using the operators for map
<!>and apply<*>Given we can replace the 1st combination of
pureandapplywithmap
Still, it's not ideal!
Applicative styles
The previous syntax is called βStyle Aβ and is not recommended in modern Fβ― by Don Syme - see its Nov. 2020 design note.
When we use the mapN functions, it's called βStyle Bβ.
The βStyle Cβ relies on Fβ― 5 let! ... and! ... in a CE like option from FsToolkit:
π Avoid style A, prefer style C when a CE is available, otherwise style B.
Applicative vs Monadic behaviour
The monadic behaviour is sequential: β The computation #n+1 is done only after the computation #n.
The applicatives behave in parallel: β All the computations for the arguments are done before applying them to the wrapped function.
π Even if monads can do more things, applicatives can be more performant on what they can do.
Applicative parallel behaviour
The corollary is about the Result type and its bind function:
β As soon as the current result is an Error case, f is ignored.
β On the 1st error, we "unplug".
Given the Result<'ok, 'error list> type, apply can accumulate errors:
βοΈ Notes:
Errors are either accumulated (line 6) or propagated (lines 4, 5).
At lines 4 and 6,
rfis no longer a wrapped function but anError. It happens after a firstapplywhen there is anErrorinstead of a wrapped value (lines 5, 6).
π‘ Handy for validating inputs and reporting all errors to the user. π Validation with F# 5 and FsToolkit, Compositional IT
Wrap up
Functional patterns key points
Monoid
+ (combine), composite design pattern ++
Functor
map, preserve structure
Monad
bind, functor, flatten, sequential composition, effects
Applicative
apply, functor, multi-params function, parallel composition
Functional patterns in Fβ―
In Fβ―, these functional patterns are applied under the hood:
Monoids with
int,string,listand functionsMonads with
Async,List,Option,Result...All patterns when using computation expressions
βοΈ After the beginner level, it's best to know the principles of these patterns, in case we need to write computation expressions.
π€ Make these patterns more explicit in Fβ― codebases?
Meaning: what about Fβ― codebases full of monad, Reader, State...?
Generally not recommended, at least by Don Syme
Indeed, the Fβ― language is not designed that way.
Although, libraries such as FSharpPlus offer such extensions to F#. π
To be evaluated for each team: idiomatic vs consistency βοΈ β Examples:
Idiomatic Fβ― recommended in .NET teams using both Cβ― and Fβ― code
Functional Fβ― can be considered in FP teams using several functional languages: Fβ―, Haskell, OCaml...
Additional resources π
"Understanding monoids" series βF# for Fun and Profit
"Map and Bind and Apply, Oh my!" series βF# for Fun and Profit
Applicatives IRL βJeremie Chassaing
Last updated
Was this helpful?