Side effects and how to deal with them the cool way, Part 1: Pure functions and functors

Introduction

Functional programming has become very popular, and one of the concepts that have gained popularity with it is the concept of "side effects", but what are really side effects and why functional programming is so good at handling them?

I would first argue that most of the power of functional programming comes from leveraging pure mathematical concepts into the world of software engineering; a lot of people start getting scared by the only mention of mathematics, but do not worry! Math is cool, and easier than you would think if given patience, just trust me for now. Math gives us a lot of power, power that we can use to create reliable, scalable, error free, and simply right software.

Lets first talk of one of those powerful and simple concepts that can greatly be used in software engineering: pure functions.

Pure Functions

function: In mathematics the definition of a function is a relation from a set of things A to a set of things B, and for each thing A there must be only one thing B related.

And why is it useful? Because it is completely deterministic, if you have a function Int⇒Int you know that it will only accept integers, and that it will always give back an integer, and this actually communicates a lot, for example lets read this function:

// Boolean => Boolean
def f(x: Boolean): Boolean

How many implementations of this function can there possible be? Only two, either the identity function, or the negation function, because if you have a boolean, the only thing that we can do with it is either leave it like that or change it to the only other possible value.

Another useful thing that comes with this determinism is function composition (which wont be explained here, but if you don’t know what it is, please! search it on the internet and be amazed :) ). It is guaranteed that the new function produced by the composition will work thanks to the determinism of its domain and codomain (what it receives and what it returns). And by composing functions is how we create programs.

Now in functional programming a pure function is just a function which respects the definition of a formal mathematical function, and by doing this we gain all the advantages (properties by formal proofs); but alas! the real world doesn’t work like that, we are surrounded by indeterminism! Values could be null, exceptions are thrown, IO may fail! And a pure function simply cannot accept a null value, or return an exception, because if a pure function says it will return an Int, then an Int is what it will return! Otherwise it simply is not a function, and we lose all the promised properties and advantages. And this little demons my friends are called side effects, what ever that possibly breaks the function signature is a side effect (possibly = indeterminism) (function signature = domain and codomain).

Handling Side Effects v1: A map function

So we could do all kind of mumblejumble inside our functions to try to handle specific side effects (like try-catch statements for handling errors), but that is not extensible or even nice to read, we want to capture the essence of a side effect, keep them in a context, and then somehow apply our pure functions to it, so that our code stays clean and easy.

Lets start with the possible missing value side effect (a.k.a. null pointer exception), in this case we want to apply our pure functions ONLY when the value actually exists, since we are using scala lets abstract it into a trait and two specific sub types:

trait Maybe[+A]
case class Just[A](value: A) extends Maybe[A]
case object NotThere extends Maybe[Nothing]

Just is the type that represents and existent value, and NotThere the one for when it is missing, this, as you can see is a proper abstraction of the side effect.

Now lets code a simple pure function for integers:

// Int => Int
def plusOne(x: Int): Int = x+1

As you can see our pure code is completely decoupled from the side effect code (this is what functional programmers like to brag all the time about, pushing the side effects from our pure code). But now in order to use our pure function with a Maybe integer we need to add some methods.

trait Maybe[+A] {
	def value: A
	def isDefined: Boolean
	def map(f: A=>B): Maybe[B] = {
		if(isDefined) Just(f(value)) else NotThere
	}
}

case class Just[A](value: A) extends Maybe[A] {
	def isDefined: Boolean = true
}

case object NotThere extends Maybe[Nothing] {
	def isDefined: Boolean = false
}

So the important function that we added here is map(f: A⇒ B): Maybe[B], as you can see is a higher order function because it takes an other function (our pure functions), and will only apply them to the value if and only if the value is defined. Now lets make a useful companion object: And using our plusOne pure function, what we can do is something like this:

val x: Int = 8
val maybeX = Maybe(x).map(plusOne)
> maybeX: Maybe[Int] = Just(9)

Now what if x was actually a null value? well then maybeX: Maybe[Int] = NotThere and no horrible null pointer exception is thrown, also we can handle that or exit the Maybe wrapper like this:

val x: Int = 8
val y = Maybe(x).map(plusOne) match {
	case Just(i) => i
	case NotThere => 0
}
> maybeX: Int = 9

Now lets realise that our true operation (the plusOne function) was kept nice and pure, and handling null values is now completely decoupled from all the countless functions that potentially could have ended with a terrible:

if (x == null)
...else ...

Also do not worry about coding all of these, since Scala already comes with these implemented:

Maybe[A] = Option[A]
Just(a)  = Some(a)
NotThere = None

Handling Side Effects v2: Functors

There is yet another spicy concept from functional programming that we will talk about: Functors. And we actually just created one! The formal definition of a functor is actually a little bit misleading as how they are used in software engineering, so lets keep it informal for now (as much as it pains).

If a normal function is a mapping from a thing A to a thing B, then analogously a functor is a mapping inside a higher kinded type F[A] to F[B]. In this case the F[_] is the "higher kinded type" because it requires another type to become a type itself, for example our Maybe[\_] type. The Maybe map function is what it makes it a functor, since we are in the end going from some Maybe[A] to another Maybe[B].

We could even abstract and decouple the concept of a Functor like this:

trait Functor[F[_]] {
	def map[A, B](fa: F[A])(f: A => B): F[B]
}

You can see this as a functional design pattern, using Functors to decouple side effects from our pure functions. Other functors implemented by the scala library are:

List[_]
Future[_]
Try[_]
Either[_, _]

And with functional libraries like scalaz or cats you will find many more that handle other type of side effects.

Conclusion

Side effects can become the real arch enemy of programmers, but with powerful functional design patterns we can control them and create type safe, reliable programs. We will see in the next post an even more powerful design patter that derives from the need of controlling side effect, the always famous Monad.