1. Introduction
Monads are one of the most important building blocks of functional programming. They help us to compose various computations in a better way by structuring the program generically and avoiding boilerplate code.
The Writer monad is one of the sub-categories of the Monad type. In this tutorial, we’ll look at the Writer monad in detail using the Cats library.
2. What Is a Writer Monad?
The Writer monad represents a computation that produces a value along with a description of the computation. It’s represented as type Writer[L, A], where L represents the computation description, sometimes called the log side of the Writer. Type A represents the actual value of the computation.
We can accumulate the descriptions of computation along with the computed value using Writer monad. This is very useful in cases where we want to track and reason about a chain of computations.
We should note that Writer[L,A]* is a type alias for *WriterT[Id, L, A] where WriterT is a monad transformer. However, for the scope of this article, it’s not necessary to have knowledge about monad transformers.
3. Setup
Let’s add the Cats library dependency in build.sbt:
libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0"
4. Instantiating Writer Monads
Let’s create an instance of a Writer monad:
val writer: Writer[String, Int] = Writer("Multiplication", 5 * 5)
The first argument of the Writer monad is the description of the value/computation. The second argument is the actual computation that will be performed. We can get the description and value out from a Writer monad using the run() method, which returns a tuple:
val (desc, value) = writer.run
value shouldBe 25
desc shouldBe "Multiplication"
5. Operations
In this section, we’ll look at various operations that can be performed on instances of the Writer monad.
5.1. map() and flatMap()
Since Writer is a monad, we can apply map() and flatMap() methods just like any other monad. For instance, we can use map() on an existing Writer instance, that operates only on the value side:
val writer = Writer("Number", 5)
val (log, value) = writer.map(_ * 2).run
value shouldBe 10
log shouldBe "Number"
Since map() and flatMap() are available, we can use for-comprehension as well:
val combined: Writer[String, Int] = for {
writer1 <- Writer("Init Value,", 10)
writer2 <- Writer("Multiplication", 5)
} yield writer1 * writer2
combined.run shouldBe ("Init Value,Multiplication", 50)
When we compose multiple Writer instances, the log side of each writer is combined automatically using a SemiGroup type-class for the String type. We can notice that the log messages combined together into a single string, without any explicit operation.
5.2. Log Side Operations
The Writer type provides additional methods to operate only on the log side. Let’s explore them with the help of some examples.
The tell() method appends a value to the left side of the Writer using the available SemiGroup type-class instance:
val writer = Writer("Init Value", 100)
val writer2 = writer.tell(",Starting manipulations")
writer2.run._1 shouldBe "Init Value,Starting manipulations"
Similarly, we can clear the left side data using the method reset():
val writer = Writer("Init Value", 100)
val resetWriter = writer.reset
val (log, value) = resetWriter.run
value shouldBe 100
log shouldBe empty
5.3. Other Operations
Writer also provides more operators to transform the data within the instance. For instance, we can extract only the value side from the Writer instance using the method value():
val writer = Writer("Log Side", 100)
val value: Int = writer.value
value shouldBe 100
Additionally, we can swap the sides using the swap() method:
val writer = Writer("Log Side", 100)
val (log, value) = writer.swap.run
log shouldBe 100
value shouldBe "Log Side"
6. Pros and Cons of Writer Monads
In this section, let’s look at some of the pros and cons of using Writer monad.
Let’s see some of their benefits:
- ability to describe the operation along with the final values
- utilize the power of monads for easier composition
There are a few cons when using the Writer monad. Even though the left side is called the log side, it shouldn’t be confused with the log statements in the traditional logging sense. The reason is that the left side of the Writer is meant to hold the accumulated description of operations, rather than log statements. Moreover, the log side will keep on accumulating until the run() method is executed. As a result, it doesn’t actually log these statements in real time.
To perform referentially transparent logging, it’s better to use logging libraries such as log4cats.
7. Conclusion
In this article, we looked at the Writer monad in Cats. We also discussed how we can utilize the Writer monad to describe chained operations. Additionally, we touched on some of the benefits and challenges of using it. The Writer monad is extremely useful in documenting and troubleshooting a chain of operations with ease.
As always, the sample code used in this article is available over on GitHub.