← Previous in this series: Type Parrameters

It’s common for people who are not very familiar with functional programming to say that in functional programming you don’t do side effects. But actually, performing side effects, is one of strengths of the functional approach to programming, and it’s when you have a complex enough system with lots of side effects happening, that FP shines.

What Are Side Effects?

Before we go any further, we need to define what are side effects. Let’s define them in terms of desirable properties of our code. In chapter three, we talked about pure functions. One emergent property of a pure function is something called referential transparency. Being referentially transparent, means that an expression can be replaced by its result value without changing the meaning of the programme. This is a highly desirable property to have in a codebase, as it allows for local reasoning, I.E. you don’t need to leave the scope of the current function to understand what it is doing.

Knowing this, it becomes very easy to define side effects. A side effect is anything that breaks referential transparency. This means that anything that a function does which causes an observable consequence outside of it (other than, of course, that which is caused by the run-time in order to run the code), or that causes the function to depend on observing external state, breaks referential transparency, and so that function performs side effects. For example, in languages with the concept of class instances, or variables with a scope that escapes the closed function, changing or reading one such variable/instance property, is a side effect. Furthermore, reading or writing from a file on disk, accessing a database, generating a random number (unless the seed is provided to the function, and the result depends exclusively on the seed), or fetching the current date/time, all are side effects.

Effects

The definition of effects, is much less formalised that the one for side effects. Usually in the context of Haskell and languages where a similar approach to functional programming is taken, such as Scala, effects refer to an abstraction that allows us to perform side effects while preserving referential transparency throughout our codebase.

As we mentioned previously, there’s a lot of value to be gained from making sure our functions are pure and total. If we just perform ad-hoc effects then we have neither of these properties. So how can we perform those effects then?

If you’ve done any Haskell, you’ve noticed how the main entry function will have a type IO () (IO of Unit). In Scala similar types are provided by libraries, amongst them are cats.effect.IO , scalaz.zio.ZIO , and monix.eval.Task . These are called Effect types, because they are types that represent, and allow us to abstract at the level of effects, or as per our definition above, allows us to perform side effects in a pure way. This might sound like a contradiction in terms, but we’ll explain in a moment.

In this article, I’ll be using cats.effect.IO , which going forward, I’ll be referring to as IO . This is because it’s the one I’m most familiar with. Also, it is part of an ecosystem, that provides the type classes and type class instances that allow us to abstract over it.

Referential Transparency

Let’s imagine the following simple programme:

import scala.util.Random def addCharTo ( s : String ) : String = s "${s}${Random.nextPrintableChar}"

When we call addCharTo , here’s one possible result:

addCharTo ( "a" ) // String = aF

We said that referential transparency means we can replace a call to a function by it’s return value without altering the meaning of the programme (I.E. we get the same result). Lets see if this is true for this programme. Our call to addCharTo , returned "aF") . If our programme is referentially transparent, then we should be able to replace addChar("a") with s"a${Random.nextPrintableChar}" and still get "aF" as a result. Let’s see what happens:

s "a${Random.nextPrintableChar}" // String = aJ s "a${Random.nextPrintableChar}" // String = aY s "a${Random.nextPrintableChar}" // String = a^ s "a${Random.nextPrintableChar}" // String = aT

Not only we didn’t get "aF" but each time we called s"a${Random.nextPrintableChar}" we got a different result. So we can say that addChar is not referentially transparent, nor pure, as the reason we’re losing referential Transparency here is because the result doesn’t depend exclusively on the input of the function, but instead it depends on side effects.

Lets re-write this code using IO:

import scala.util.Random import cats.effect.IO def addCharTo ( s : String ) : IO [ String ] = IO . delay ( s "${s}${Random.nextPrintableChar}" )

Now, when we call addCharTo , this is what we get:

addCharTo ( "a" ) // cats.effect.IO[String]

No matter how many times we call it, we’ll always get a new value of type IO[String] . So now, addCharTo is not performing a side effect anymore, and the return result is referentially transparent. addChar now returns a description of how to perform the side effect, instead of the performing the side effect. Now we can trust it to always return the same description for the same parameters. Meaning, that we can replace the function call with its return value without changing the meaning of the programme:

import scala.util.Random import cats.effect.IO def addCharTo ( s : String ) : IO [ String ] = IO . delay ( s "${s}${Random.nextPrintableChar}" ) addCharTo ( "a" ) // cats.effect.IO[String] IO . delay ( s "a${Random.nextPrintableChar}" ) // cats.effect.IO[String]

Now addCharTo is pure, and referentially transparent. At this point, you might be thinking that this doesn’t look that useful, since we got purity and referential transparency, at the cost of getting back something that is not the result of the side effect we want to perform. This section focused purely on the mechanics of how IO works, so let’s take a look at how and why this is useful, and the reasoning behind this approach.

Structure and Interpretation

So we made our addCharTo function pure and referentially transparent, but now, it also doesn’t seem to be performing the side effect we wanted. Also, while the impure version of it returned a string that we could use, the pure version returns an IO , how is this helpful then?

When we moved our side effect into a call to IO.delay , we suspended our side effect. What this means is that we provided a description of how the side effect will be performed, but we didn’t actually perform it. A less than perfect analogy could be that now the function returns the recipe to perform the side effect, while the implementation without IO was cooking the dish. The effect is in “suspension” until we call one of the “unsafe” functions on the IO , at which point, the description we provided is interpreted and the side effect runs. This little trick allows us to push the execution of our side effects to the top of our application and deal exclusively with pure code, up to the point where we run one of the so called unsafe operations.

Now, for this to be of any use, we have to have ways to act on our pure value of IO. That’s where a few functions you might have heard of before come into play. In order for IO to be useful, we must be able to map and flatMap (called bind in Haskell and represented by the >>= operator) over IO (there’s a few more that are required but for now let’s focus on these two). These functions exist and have some well defined behaviour because IO has a Monad and a Functor instance. We won’t go into details on that but if you read the Learning Scala article, there’s a few resources there that focus on more details about those. What map allows you to do, is to append a transformation to your computation. For example, if you have a computation represented as IO[String] , you can map over it to append a transformation that turns the String into h4ck3r wr1t1ng. On the other hand, flatMap binds two IO s together by sequencing them. This has a vital part in making sure that your side effects run in order, and that the programme ends in a single IO , so that at the end you only need to run a single unsafe operation. Better yet, in Haskell , or if you use cats.effect.IOApp , you just need to return the result of sequencing all IO operations from your application entry point, meaning that nowhere in your code there’s need for impure functions.

Both Haskell and Scala provide syntactic sugar for chaining map and flatMap calls, respectively called do notation and for comprehensions. These allow us to more easily sequence and transform effects:

import scala.io.StdIn.readInt def readNumber : IO [ Int ] = IO ( readInt ) def print ( s : String ) : IO [ Unit ] = IO ( println ( s )) val prog : IO [ Int ] = for { _ <- print ( "Enter a number: " ) a <- readNumber _ <- print ( "Enter another number: " ) b <- readNumber } yield ( a * b )

You may have noticed that we’re using IO(<code>) instead of IO.delay(<code>) here. This is because the apply implementation for IO simply calls delay , so both do the same thing. This is copy/paste from the actual IO implementation:

object IO extends IOInstances { /** * Suspends a synchronous side effect in `IO`. * * Alias for `IO.delay(body)`. */ def apply [ A ]( body : => A ) : IO [ A ] = delay ( body ) (...) }

As per the example above, we’re creating prog by sequencing IO s, then applying a transformation. We can then in turn do the same with prog and any other IO s existing in our programme, ad infinitum. This makes it very easy and convenient to compose these mini-programmes into larger ones up to the point where we have a top level IO that represents our whole programme.

This kind of structure also makes it easy to restart parts of the programme when failures occur, since now we deal with our programme as values that we can handle as we see fit. This approach, allows you to think about your programme locally, because all the operations on the results of your effects are defined in terms of appending transformations or sequencing it to another effect.

Conclusion

All of this might be a bit to digest. The best way to get a feeling of how this all works, is to play with it, an build some simple application using IOApp , making sure that you never call any of the unsafe operations on IO.

In the wild, in Scala, you’ll rarely see applications that use IO directly, except at the very entry point of the application, and everything else will be abstracted over a higher kinded type parameter. We’ll look further into that on the next chapter.

Next in this series: Abstracting Over Effect Types →