|
From Get Programming with Scala by Daniela Sfregola After reading this article, you will be able to:
|
Take 37% off Get Programming with Scala by entering fccsfregola into the discount code box at checkout at manning.com.
In this article, you’ll learn about purity: a fundamental principle of functional programming. In particular, you’ll see that a pure function is total and it has no side effects: you’ll discover what these terms mean in detail. Distinguishing between pure and impure functions can help you identify and prevent bugs in your code. For example, consider the following scenario:
Suppose you are developing the software for a smart thermostat. Your business requirements dictate that your thermostat never reach temperatures below freezing (0°C or 32°F) because it could damage its mechanical parts. If this happens, your program should trigger an emergency recovery plan that changes the target temperature to a default value that the user can configure. You could translate this with the following function:
def monitorTemperature(current: Double, recovery: Double): Double =
if (current >= 0) current else recovery
This function monitorTemperature
behaves in different ways depending on the “purity” of its parameters. Consider the following function invocations:
scala> monitorTemperature(current = 5, recovery = 10)
res0: Double = 5.0
scala> monitorTemperature(
current = 5,
recovery = {println("EMERGENCY! triggering recovery"); 10})
EMERGENCY! triggering recovery
res1: Double = 5.0
Both function calls are valid, but the second one behaves unpredictably: even when the current temperature is above the freezing threshold, an unexpected (and confusing!) message appears in the console.
Consider this: Can you think of another example in which your function may suffer from some unexpected behavior because of the possibly impure values you have passed as parameters?
A definition of Purity
First, let’s discuss purity and how it differs from impurity. A pure function is total, and it has no side effects. Let’s see what each of these terms mean.
TOTALITY
A function is total if it is well-defined for every input: it must terminate for every parameter value and return an instance that matches its return type.
Let’s consider the following functions:
def plus2(n: Int): Int = n + 2 // total
def div(n: Int): Int = 42 / n // non-total
def rec(n: Int): Int = if (n > 0) n else rec(n - 1) // non-total
The function plus2
is total because for every possible integer passed as its parameter, it terminates, and it returns an integer value as the result of its evaluation. The div
function is not total because even if it always ends, it doesn’t always return a value of type Int
: it throws an ArithmeticException
its parameter n equal to zero. A thrown exception is an unexpected value not represented in its return type. Finally, the rec function is not total because it never terminates for any integer less or equal to zero.
QUICK CHECK 1
Which ones of the following functions are total? Why?
def opsA(n: Int): Int = if(n <= 0) n else n + 1
def opsB(n: Int): Int = if(n <= 0) n else opsB(n + 1)
def selectException(predicate: Boolean): Exception = if (predicate) new IllegalStateException("msg here") else new ArithmeticException("another msg here")
def anotherToString(obj: AnyRef): String = { Thread.sleep(1000) // measured in millis obj.toString }
def validateDistance(dist: Double): Double = if (dist < 0) { throw new IllegalStateException("Distance cannot be negative") } else dist
SIDE EFFECTS
A side effect is an operation that has an observable interchange with elements outside its local scope: it affects (i.e. write side effect) or is affected by (i.e. read side effect) the state of your application by interacting with the outside world. A few examples are the following:
def negate(predicate: Boolean): Boolean = !predicate // no side effect
class Counter {
private var counter = 0
def incr(): Unit = counter += 1 // (write) side effect
def get(): Int = counter // (read) side effect
}
def hello(name: String): String = {
val msg = s"Hello $name"
println(msg) // (write) side effect
msg
}
The function negate has no side effects: its only instruction acts on its parameter to produce a return value. The function Counter.incr
contains a (write) side effect: every time you invoke the function, it changes the assignment for the variable counter, which is a code element that lives outside of its local scope. Counter.get
also has a (read) side effect: given the same input, it returns a different integer depending on the variable counter’s current assignment. The function hello has a (write) side effect because its println instruction produces a message into the console, a component shared across your application that lives independently from its local scope.
QUICK CHECK 2
Which ones of the following functions have side effects? Why?
1. def div(a: Int, b: Int): Int = {
if (b == 0) throw new Exception("Cannot divide by zero")
else a / b
}
2. def getUserAge(id: Int): Int = {
val user = getUser(id) // gets data for a database
user.id
}
3. def powerOf2(d: Double): Double = Math.pow(2, d)
4. def anotherPowerOf2(d: Double): Double = {
println(s"Computing 2^$d...")
Math.pow(2, d)
}
5. def getCurrentTime(): Long = System.currentTimeMillis()
QUICK CHECK 3
A pure function is total and has no side effects. Consider the code snippets provided in Quick Check 1 and Quick Check 2: which ones of them are pure?
Differentiating between pure and impure functions
In the previous section, you discovered that a function is pure if total, and without side effects. You can describe this concept in a less formal way as follows: a function is pure if nothing else but its parameters determine its behavior, which its return type entirely describes (see figure 1).
Figure 1: A visual representation of the differences between pure and impure functions. Given an input, a pure function returns an output. On the other hand, an impure one also produces additional effects not represented in its return value.
A pure function guarantees that it always returns the same output given the same input parameters. In other words, you can replace its invocation with its return value and obtain the same outcome: This concept is called “referential transparency”, and it has several practical implications.
Suppose you have the following two functions, called pureF
and impureF
, which take a string as their parameter and return another string as the result of some computation:
def pureF(name: String): String = s"Hi $name!"
def impureF(name: String): String = {
println("...doing something here...")
s"Hi $name!"
}
The function pureF
is pure, while the function impureF
is impure.
You can substitute the function call pureF(“Bob”) with the string “Hi Bob!”. On the other side, swapping the function call impureF(“Bob”) with the string “Hi Bob!” would not produce the same result because the print instruction in the console would be missing.
Functions with no parameters: parentheses or no parentheses?
When declaring a function with no parameters, you should omit the parentheses if the function is pure (i.e., def f = ???). Vice versa, you should specify them if the function is impure (i.e., def f() = ???).
This rule is a style suggestion rather than a law imposed by the compiler.
QUICK CHECK 4
Which ones of the following statements are true?
- Pure functions do more than just computing a value
- You can replace calls to impure functions with their return value without losing functionalities.
- Pure functions are total
- A function that throws exceptions is pure
- A function with side effects is impure
Summary
In this article, my objective was to teach you about the functional concept of purity. - You have learned that pure functions are total and have no side effects.
- You have also discovered referential transparency and how you can use it to differentiate between pure and impure functions.
Let’s see if you got this!
TRY THIS:
Which ones of the following functions are pure? Which ones are impure?
def welcome(n: String): String = s"Welcome $n!"
def printWelcome(n: String): Unit = println(s"Welcome $n!")
def slowMultiplication(a: Int, b: Int): Int = { Thread.sleep(1000) // 1 second a * b }
def saveUser(user: User): User = { insertUser(user) // inserts in a database user }
def getUser(id: Int): User = { selectUser(id) // searches in a database }
Answers to Quick Checks
QUICK CHECK 1
The answers are as follows:
- The function
opsA
is total because it always terminates and returns an integer for every integer passed as its parameter. - The function
opsB
is not total: it calls itself recursively and never terminates for positive integers. - The function
selectException
is total because it returns an instance of exception: it computes a value that matches its return type for every input. The keyword throw is missing, so the function does not throw the exception, but it returns it as a class instance. - The function
anotherToString
is total: for every input, it eventually terminates after sleeping for 1 second (or 1000 milliseconds) and returning a string. What if the function was to block for a much more extended period (e.g., ten years), would you still consider it total? - The function
validateDistance
is not total because it throws an exception for any negative double number.
QUICK CHECK 2
The answers are the following:
- The function
div
has no side effects: throwing exceptions is not a side effect because it does not change the state of components external to the function. - The function
getUserAge
returns different results depending on which objects are in the database, which is a (read) side effect. - The function
powerOf2
has no side effects as its return value depends entirely on its input. - The function
anotherPowerOf2
has a (write) side effects: every time you call it, it produces a new message to the console, changing its state. - The function
getCurrentTime
returns a value that depends on your machine’s internal clock, which is a (read) side effect.
QUICK CHECK 3
In Quick Check 1, the functions opsA
, selectException
, anotherToString
are pure. In Quick Check 2, there is only one pure function, which is powerOf2
.
QUICK CHECK 4
The answers are as follows:
- False
- False
- True
- False
- True
That’s all for this article. If you want to learn more about the book, you can check it out on Manning’s browser-based liveBook platform here.