A few years ago, I started learning F#. That took me to the path of building a full compiler for a pure functional language in F# using only a purely functional approach.
After that, I have moved to Scala and never came back to the .NET Framework. However, sometimes I miss the simplicity and expressivity of F#. I have demonstrated so in some of the posts I have written before such as (|> Operator in Scala, Higher order functions, what are they? and What Comes Next to Higher Order Functions).
Coming back to F# today just to miss some Scala constructs I really use a lot, the Monad Writer.
Logging is an important part of our applications. However, we rarely do pure functional logging. I am not going to go over why or why not to do functional logging, but we are going to see how simple is to write and use pure functional logging in F# by using a Monad Writer.
The first thing we need to do, it’s a way to create a Writer. Specifically, we want to be able to get access to the initial writer that we are going to use for our next operations.
This should be as simple as:
let writer = bind 5 "starting"
The bind
operation should return a Writer which initial value is 5
and the initial value to be logged is starting
.
In order to do this, we need to define how a Writer looks like.
type Writer<'a, 'L> = AWriter of 'a * List<'L>
A simple discriminated union is more than enough. We need a generic type ‘a
and the generic ‘L
which is our log. Of course, we are not constrained to log only string
. The elements of the log can be, in fact, of any type. For convenience and simplicity, we are going to use string
most of the time, but we could use any other type as part of the log.
At this point, we only need the bind
function so it creates the desired Writer.
let bind = function| (v, itemLog) -> AWriter(v, [itemLog])
bind
is a function that receives two args, a generic value v
and the first item to be logged, itemLog
and then it returns our initial Writer.
Now, we need a way to map over the value of the Writer, so we can change types of the boxed value without affecting the log. Let’s see how we can write map
let map fx = function| AWriter(a, log) -> AWriter(fx a, log)
In here, map
takes another function fx
that is applied over the Writer value a
by creating a new Writer. Notice we never mutate a Writer, we just create new Writers.
With only these two, we can start defining other functions that return Writers (doing logging) while keeping referential transparency.
Let’s define some functions we could use in our program.
let sum x y = bind (x + y, "sum")let mul x y = bind (x * y, "mul")let mod1 x y = bind (x % y, "mod")let minus x y = bind (x - y, "minus")
Notice all these functions only have one single responsibility, to sum, to multiply, to find the module, and calculate difference. They have no knowledge of global loggers and they don’t append anything to a shared state. They are very small, very simple to test; they are pure functions.
As we can see, all of them return a Writer through the function bind
. Thanks to the advance F# type system and type inference, defining functions like this is a very easy task.
By the using of map
we can do transformations as follow.
let str a = a.ToString()
let to_string a = a |> map str
sum 5 5 |> to_string
Let’s take a closer look at this part.
First, we created a Writer with value 5 + 5 = 10
and log ["sum"]
and then we call to_string
which basically calls map
so the result is a Writer with value "10"
and the same log ["sum"]
. We have modified the value of the Writer via map
without touching the log.
What about getting things out of the Writer?
Let’s define a simple way to extract the current value and the log we have been constructing so far.
let run = function| AWriter(a, log) -> (a, log)
run
is a function that receives a Writer and returns in a tuple form the value and the log. Now we could take a look at the content of the Writer.
let (v, log) = run (sum 5 5)
or in more idiomatic F#
let (v, log) = sum 5 5 |> run
In here, v
is the value 10
and log
is ["sum"]
.
Previously, we have defined different functions where all of them return Writers (sum, mul, mod1, minus), yet we don’t have a way to combine the defined operations so the result of each of them gets aggregated into a single log.
The missing operation is flatMap
.
let flatMap fx = function| AWriter(a, log) ->let (v, new_log) = fx a |> runAWriter(v, List.append log new_log)
flatMap
receives a function of type 'a -> Writer(b, List<`L>
and returns a new Writer. If we take a closer look, flatMap
is the one in charge of aggregating the logs from multiple Writers.
Let’s look at an example which could be a little more enlightening.
let result =sum 5 5|> flatMap (mul 2)|> flatMap (mod1 25)|> flatMap (minus 10)
The final value, result
, is a Writer where we can call run
.
let (v, log) = run result
System.Console.WriteLine v
for i in log doSystem.Console.WriteLine i
This will print out:
5summulmodminus
Where 5
is the result of 5 + 5 = 10 * 2 = 20; 25 % 20 = 5; 10 — 5 = 5
and the the log in the order operations were executed.
At this point, we have fully defined a Monad Writer in F# in it’s simplest way. We could actually add more functionality to it, but let’s keep it as simple as possible for this exercise.
The entire code of Writer looks like this
module MonadWriter =
type Writer<'a, 'L> = AWriter of 'a \* List<'L>
let bind = function
| (v, itemLog) -> AWriter(v, \[itemLog\])
let run = function
| AWriter(a, log) -> (a, log)
let map fx = function
| AWriter(a, log) -> AWriter(fx a, log)
let flatMap fx = function
| AWriter(a, log) ->
let (v, new\_log) = run (fx a)
AWriter(v, List.append log new\_log)
The Monad Writer is a very elegant way to keep your functions pure while they have a single responsibility, such doing small operations like a + b
. Also, by following this p_attern,_ we avoid injecting loggers all around our code, or worse, accessing to global loggers which can be very dangerous when used in multithreading / parallel execution contexts.
Most of us when working on OO languages have used the bad loggers, but that only makes deeper dependencies in code while breaking the Single Responsibility Principle.
It is or solely decision to move to a functional way of doing logging, especially in distributed systems. You can take a look how to do in Apache Spark by reading How to log in Apache Spark, a functional approach.
Log safe, be functional.