All Articles

Pure Functions

Pure functions

What is the mathematically formal definition of a function?

Given sets X,YX, Y (referred to as the domain and codomain, respectively), a subset fX×Yf \subset X\times Y is a function if it satisfies the following two properties.

Prop 1

xX yY:(x,y)f\forall x \in X \ \exists y \in Y : (x,y) \in f

In words, every admissible input has a well-defined output.

Prop 2

xX y,yY:(x,y),(x,y)fy=y\forall x \in X \ y,y' \in Y : (x,y), (x,y')\in f \Rightarrow y = y'

In words, each input can have at most one output.

The first thing you usually learn about in functional programming is the notion of a pure function. What is a pure function? A pure function is, firstly, a function that conforms to the above definition. That is, a function which satisfies the two properties:

  • Each admissible input has a well-defined output
  • Each input has at most one output

It is relatively straightforward to construct a function which violates one of the above two properties.

Haskell

module Main
    ( main, f
    ) where

main :: IO ()

f :: Int  -> Int
f x = x `div` x

main = print (show (f 0)) -- divide by zero

The above function violates property 1. Zero is an admissible input with an ill-defined output.

JavaScript

let f = a => Math.random()

The above function violates property 2. It returns a different (with high probability) value for a fixed argument a.

Thou shalt not pass

There are some additional points to be made about pure functions. Points that are taken for granted in the world of mathematics. (Note that the three examples below are in C).

A function as defined above should effectively be blind to any variables which are not explicitly passed to it as arguments or which have not been instantiated within its body. Obvious, right?

int a = 0;

int f() { return a; }

Maybe not so obvious…

The above function is permissible in many languages. What makes it impure?

The function is acting on a variable outside of the scope of its arguments and body. The function below would be a correction.

int f() {
  int a = 0;
  return a;
}

This is known as referential transparency.

Thou shalt not mutate

int a = 0;

int f() {
  a = 1;
  return a;
}

The function above not only acts on a variable outside of its scope, it also mutates it. A pure function is not permitted to mutate the state of a variable outside of its scope.

In fact, even keeping variables inside the scope of a function does not in itself protect those variables from mutation. One may pass a variable by reference as an argument to the function, in which case it is in-scope and yet mutable by the function.

int f(int *a) {
  *a = 1;
  return *a;
}

int main() {
  int a = 0;
  f(&a);
  printf("%d\n", a);  // 1
  return 0;
}

Here we are passing arguments to the function by reference, making the arguments mutable. So, as far as the function is concerned the variable is in scope yet mutable.

The takeaway here is thou shalt not mutate variables (arguments or otherwise) and thou shalt not act on arguments not explicitly passed or internally instantiated.

Below is a demonstration of what is arguably the simplest pure function, the identity function, in several languages.

Haskell

f :: Int  -> Int
f x = x

Scala

def f(x: Int): Int = x

Rust

fn f(x: i64) -> i64 {
    return x;
}

JavaScript

const f = x => x

Clojure

(defn f
  [x]
  x)

Elm

f x = x
Published 7 Jun 2018