Connecting...

Pexels Photo 96381

Effects by Marco Lopes

Pexels Photo 96381

Performing side effects is a strength of functional programming!

But what are side effects? How do you define them?

Marco Lopes answers all our effects questions! Check it out.


'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:

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

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:

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:

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

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:

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:

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:

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.'


This article was written by Marco Lopes and originally posted on marco-lopes.com