Impure functions

Functions that return the same output when given the same input are called pure functions. Then what are impure functions?

Most of the functions we have written so far are stateless. That means if you pass in the same inputs, you always get the same outputs. Also, they do not do anything else apart from returning the value.

def add(a, b):
    return a + b
    
add(2, 3) # 5
add(2, 3) # 5
add(2, 3) # 5

Such functions are called pure functions or stateless functions or side-effect free functions. The output of the function depends only upon the input. If the input is the same, the output will also be the same.

Stateful functions

Most of the time, functions are stateless. But you can also create stateful functions. The code below defines a higher order function called once.

def once(fn):
    called = False
    def inner(*args, **kwargs):
        nonlocal called
        if not called:
            called = True
            return fn(*args, **kwargs)
    return inner    

once_add = once(add)
once_add(2, 3) # 5
once_add(2, 3) # None

You give once a function as input, and it returns the function inner as output. When you call the returned function, it calls the underlying function and returns the result. In the example above, calling once_add(2, 3) will call the underlying function add and return 5 as the answer. But if you call it again, it will return None!

In other words, the once function takes a normal function as input and converts it to a version that can be called only once.

Notice here that once_add gives different results for the same input. Thus, once_add is a stateful function or an impure function. The output of this function does not depend only on the inputs (2, 3) but also the hidden state variable called. This hidden state variable is not a part of the inputs. Hence the function is impure.

Unlike pure functions, we cannot reason easily about impure functions. For example, what will be the output in the below two cases?

add(2, 3) # always 5
once_add(2, 3) # ?

In the first case, the output is always 5. But in the second case we cannot tell what the output will be. It all depends on whether once_add has been called beforehand or not. We need to walk through all the previous code and see what has happened previously in order to figure out what will happen here. This is the main problem with stateful functions.

However, stateful functions can also make code simpler to read by encapsulating the state. If there is not much code between creating and using once_add so that it is clear when it is called once or twice, then the stateful version can be easier to read.

Side-effecting functions

Another form of an impure function is side-effecting functions. Take a look at the add function below

def add(a, b):
    print("adding")
    return a + b

Apart from returning the sum, this function also prints a message to the terminal (this act of printing is called a side-effect as it is something that happens apart from the return value). What is the problem with side effects?

Well, assume that a programmer sees the code below

if user == "y":
    val = add(2, 3)
val = add(2, 3)
print(val)

The programmer, seeing the code above wonders why add has been called twice? Why call it inside the condition and then again do the same thing immediately outside? The programmer decides to simplify the code

val = add(2, 3)
print(val)

Do you see the bug introduced with this code change?

In the first program, if user == "y" then it will print the message "adding" twice. Once on line 2 and again on line 3. But after the simplification, the message will be printed only once. The two pieces of code are not equivalent and they give different outputs, because of the side-effect behaviour of add.

The programmer needs to understand the implementation of add before being confident of this code change. Whenever you need to know the implementation of a function before using it, then it breaks abstraction and makes the program more difficult to understand and maintain.

This is the fundamental problem with side effects. With pure functions you can always cache the outputs and avoid calling the function again etc. But when side effects are involved you cannot do this.

Still, operations like printing to the screen, geting UI events, reading and writing files, sending data over the network are all required in any program. Unless you are working on toy programs, you will always need side effecting functions.

Summary

Pure functions are easy to reason about. They don't do any funny stuff. They are easy for a programmer to read and understand. They can be cached, or run in parallel without introducing any bugs.

Stateful functions and side-effecting functions have problems, but they are also essential to any bigger program. The key to writing maintainable programs is to use these judiciously in the right places.

💡
Did you know?
Many functional programming languages allow you to code stateful functions to some extent or the other. However, Haskell is a very pure functional programming language which completely disallows stateful functions. Even the state needs to be passed into the function as a parameter.

Comments

Sign in or become a Playful Python member to join the conversation.
Just enter your email below to get a log in link.