In my last post, we discussed how to set up a simple REST service in Scala, wherein we used cats-effect IO Monad to wrap our effect-ful (or you can say side-effect-ing) code to ensure it remains functionally pure (referentially transparent).
Now anyone new to the Functional Programming world will be swarming with questions such as:
- What are side-effects and Referential Transparency?
- How does it affect my code?
- What is this IO Monad you are talking about? and How does it magically fixes my code?
At least, these were the question I had when I first encountered the IO monad. So, I thought I should put up a little something for anyone having the above questions.
Now, let’s go for it!
What is an effect/side-effect?
A side-effect is a function/expression which changes/mutates the state of something outside its local environment.
It can be something as simple as printing to console, making a DB call or a file read/write operation.
Before we go ahead, let me introduce you to Referential Transparency.
By definition, it is a property of purely functional languages that says an expression always gets evaluated to the same result, and that the expression itself can be replaced by its result without changing the behavior of the program.
You might think, what’s new about it, the below expressions:
are equivalent and hence referentially transparent.
Well, the problem starts when we have these side-effects involved in our code.
Let’s say we have defined a function demo:
This function takes in 2 arguments of type Unit and performs no computation. Now, we have the effect-ful computation stored in variable val x, and we then call the function demo:
You will observe that the above computation prints “Hello World !” on the console ONCE.
Now, let’s replace x in the function call with its value:
“Hello World !” gets printed on the console TWICE.
This clearly violates the rule of referential transparency. We got different behavior from the same function when we replace the expression by its value.
So, How can we ensure referential transparency in our program?
The current state of our code is, if we write an effect-ful expression:
The runtime just evaluates the side-effect (read eager evaluation), without giving us any chance to stop that from happening or manipulating it.
The solution to our problem comes with the ability to control WHEN these effects get evaluated.
This is where the IO Monad comes in. The IO Monad can be thought of as a wrapper around these effects, which provides us the ability to evaluate them when we need to. We’ll be using the cats-effect library in this post to demonstrate the use of IO Monad.
So let’s say we have a side-effecting function, toConsole :
We can be very sure that whenever we call the above function, we’ll always get the input String printed on the console, as it gets evaluated eagerly.
We’ll now use the cats-effect IO, to wrap this side-effect.
Now, what happens when we call this function?
Absolutely Nothing !!
Yes, nothing gets printed to the console. This is because, the return type of the function is now IO[Unit], our side-effecting code is now wrapped inside the IO and cannot evaluate unless and until we tell it so.
Cats IO provides us the method unsafeRunSync() to evaluate the encapsulated code. So to make it run we can:
Now, we can make our previous example referentially transparent as well, using our newly gained knowledge of the IO monad.
To understand IO Monad, we need to take a look at what a Monad is.
Monad is an ADT (Algebraic Data Type) that has 2 functions:
- A unit function used to place a value into the monad.
- A composition function that composes 2 monads.
(This is by no means a complete definition to a Monad, refer to this awesome video if you want to dig deep)
The representation of Monad in Scala is:
To understand this better, let’s take an example of Scala’s own Option[A] monad, and its subtypes: Some[A] & None.
The Option[A] returns Some[A] if A is not Null, and return None if A is Null.
To make this more clear let's have a function mayReturnString which returns an Option[String]:
Here mayReturnString can either be None or Some(“demo string”) but thanks to Option we don’t need to do any checks.
What’s more, we can even transform the values as we require using map / flatMap:
And as you can see, we transformed the type Option[String] — > Option[Int], without actually doing any checks for Null values, hence making our code more expressive.
So, What is an IO Monad?
IO Monad is simply a Monad which:
- Allows you to safely manipulate effects
- Transform the effects into data and further manipulate it before it actually gets evaluated.
As a side-note, the Monad also guarantees sequential evaluation during the composition of 2 monads.
Observe the definition of the composition function:
in the function f: A => F[B], we need the value of type A to transform it into F[B], which mandates that the evaluation of F[A] will be done before evaluation of F[B] because F[B] won’t even exist by then.
This property enables us to manipulate multiple side-effect-ing code, with the control of having them execute sequentially.
At last, we’ll take a look at an example, which copies the contents of one File to another. Firstly, let's define a dummy implementation of the read and write functions for file operations:
Now, using for-comprehension we can write the function to copy the content as:
And we can be absolutely sure that the evaluation of read() will always happen before the evaluation of write().
I hope I was able to draw a clear picture of what the IO Monad is, why it's needed and how it works.
There is much more to Cat’s IO, but I think this is enough to get you started.