One of the key principles of functional programming is writing pure functions. What is a pure functions? Why do we care? Why everyone is talking about it? This article will try and answer these questions.
Purity and Referentially Transparent Expressions
A pure function is composed only by referentially transparent expressions. An expression is referentially transparent if it can always be substituted with the result of its evaluation.
It’s a bit like solving mathematical expressions the same way we were doing back at school when we were younger:
– 2 * (1 + 3) becomes…
– 2 * 4 becomes…
The expressions 2 * (1 + 3) and 8 are equivalent, that’s why 2 * (1 + 3) is a referentially transparent expression.
Let’s see how we can relate the referentially transparent expression concept to pure functions!
Consider the following expression instead: println("Yo!"). When evaluating it, its result is of type Unit: the two expressions are not equivalent, so println is considered to be a non-pure function.
Let’s analyse the following scala function, called myFunc: because it is not defined for each integer value, it is considered to be a pure function.
scala> def myFunc(n: Int) = if (n > 0) n + 1 myFunc: (n: Int)AnyVal scala> myFunc(1) res0: AnyVal = 2 scala> myFunc(-1) res1: AnyVal = ()
Why Pure Functions?
One of the main principles of Functional Programming is to write our applications so that the core is made of pure functions, while side effects are in a thin external layer. Being functional is not always easy, what are the benefits of doing it?
First of all, pure functions are usually smaller and easier to understand: the human brain already thinks in a functional way! Moreover, being functional force the separation between different types of concerns: what a function does rather than how a function can be used.
Pure functions are also a lot easier to test. For example, let’s assume we have the following two function:
scala> :paste // Entering paste mode (ctrl-D to finish) def pureF(name: String) = s"Yo $name" def impureF(name: String) = println(s"Yo $name") // Exiting paste mode, now interpreting. pureF: (name: String)String impureF: (name: String)Unit
In order to test our function pureF, 1 line of code is enough: assert(pureF("Fella") == "Yo Fella"). On the other side, testing the function impureF is a lot more complicated as we need to redirect the standard output and do assertions on it.
Finally, in my opinion its biggest advantage, Functional Programming makes developers more productive. Pure functions are small and they can be easily composed together, the code duplication is minimised and it is extremely reusable. For example, consider the following two functions:
scala> def pureSum(a: Int, b: Int) = a + b pureSum: (a: Int, b: Int)Int scala> def impureSum(a: Int, b: Int) = println(a +b) impureSum: (a: Int, b: Int)Unit
Differently from the function impureSum, the function pureSum can be easily reused to sum an arbitrary sequence of numbers as following:
scala> Seq.range(0,10).reduce(pureSum) res0: Int = 45
Pure functions are one of the fundamentals of Functional Programming. This article has described what a pure function is and what are its advantages.