In previous posts (A Writer Monad for F#, How to log in Apache Spark, A Functional Approach), we have discussed the idea of using the Writer monad as a way to aggregate events. However, we were using it a simple text logger that has only a few, small differences with any of the regular loggers we could use in our application.
When looking at the Writer monad from that point of view, it seems to present an extra effort just for logging aggregation, and the implementation details can overwhelm the newcomers to the concept.
In this post, we are going to present the Writer monad as an event aggregator that can be used in a more generic way for different use cases, showing that it has a wider usability. We are also going to use C# in order to show that this concept is not only tight to the so-called pure functional languages and that it can be used on any environment we choose.
The main idea is to present a way to aggregate events using the event sourcing concepts along with the Writer monad as the main data structure to support this effort.
Event sourcing is a way to record incoming events to our system using immutable structures so that we can keep track of the events affecting the state of the system. These events should be replay-able at any time, and the continuous stream of events should converge on a system state that can be recovered or replicated by replaying the recorded events in a time series manner.
The Writer monad presents the same characteristics described above, it can record the generic events that make the current system state to change. At the same time, events are recorded within an immutable structure while the current state only changes by the application of new events.
Let’s start by looking at a simplistic example, a calculator implementation.
Our calculator can do a few operations, but it keeps track of the operation it does using the Writer monad. This initial example is similar to what we showed in our previous posts.
Notice that our calculator is defined based on a structure we call Writer
that we are going to define below. The main idea is that each operation only knows about how to create a Writer
with the operation it does.
The Writer monad can be defined as follows.
In here, we are defining the following operations.
Bind
, through the constructor, allows us to create a new Writer
.Map
allows us to change the current state.FlatMap
changes the current state while recording how the state change has occurred.Unsafe
retrieves the current state and event log.Notice that the only way to change the state is through .Map
and .FlatMap
.
Using this structure we can use our calculator in the following way.
By using .UnSafe()
we get the current state and the log of events.
In this particular case, we are using the Writer monad only as a string log, and maybe because of that, this does not look that interesting so far. However, these are the basis for our next examples.
This example shows how we can use the Writer monad to record a series of integer events happening in our system while keeping a total sum of the values receives by a stream processor.
Let’s first define our source.
As we can see, we are going to use an unbounded/infinite stream of random integers.
Now, let’s see how we use the Writer monad to receive and process these events.
This is a very clear example of event sourcing when the events are immutables and by replaying them we can get the exact same final state, which in this particular case, is the total sum of the values received.
Our final example will show how we can use the Writer monad to process events sent to a bank account. A bank account supports two basic operations, add money to it and extract money from it.
Now, suppose we have an event generator that generates transactions that we are going to be processed by our Writer monad as a stream processor.
Transactions are represented by the types Extraction
and Deposit
, these two are the event types we are going to be processing.
Now that we have a transaction source, we start by initializing our initial state.
Then, we take a number of events to process, in this case, we are interested in 100 of them, but in reality, this could be any number.
Notice that for each transaction we execute the corresponding operation over the bank account through .FlatMap
on the accountState
.
In the end, we are able to retrieve the current (final) state of the account and the events that were processed.
The interesting part is that when we start with the same initial state and then apply the same transactions on the Writer monad log to the initial state, we should end with the same final state. The state change of the value is a direct consequence of the actions recorded on the log.
For those who want to use a purely functional approach and avoid the mutation of the variable accountState
, we can add .FoldLeft
method to IEnumerable<T>
. Let’s see how.
First, we add an extension method so we can do .FoldLeft
in C#.
Then we only have to change how we process the events.
Notice that in this way we have eliminated the mutations over Writer monad, and instead, we build new ones using .FoldLeft
and .FlatMap
.
The Writer monad presents a functional approach to track changes using an immutable log that can be used in any programming language, including C#.
Also, the use of .FlatMap
allows us to chain operations in a fluent manner that presents a declarative flow control that promotes immutability.
The Writer monad, sometimes, is misunderstood, and only related to application logs, a space dominated by side-effecting libraries. However, the Writer monad is more than a logger, it is also an event source that can be used to record state changes in a clean and elegant way.
We also showed how C# can support this kind of approach, proving that Monads are not limited to the so pure functional languages.