If you are a computer scientist you will soon or later be confronted with concepts like pure functions or closures. In most cases you won’t even bother and just use those concepts without thinking much about it. With this post I will give you a short explanation what those terms mean and the concept behind them.
Pure functions
Pure functions are the easiest to understand. A function is called pure if it has no internal state. This means for a given set of inputs it will always produce the same output without any side effects.
def add(a: int, b: int) -> int: return a + b
Here the add
function takes two arguments a and b and returns the sum of both. The function itself doesn’t read or write any internal values. Pure functions don’t need to be methods inside a class and shouldn’t be. In languages like java, where everything needs to be an object, such functions can be marked as static
for example.
First-class functions
Calling a function directly can be nice and all, but in some cases it can become handy if a function could be called dynamically or dependent of the current state of execution. First-class functions can be parameters of other functions or stored in variables. This makes them the backbone of every functional language.
def add(a: int, b: int) -> int: return a + b myFunc: add myResult = myFunc(1, 2)
The add function is assigned to the variable myFunc
. Instead of calling add
directly myFunc
is treated as a function and called with the arguments 1 and 2. The variable myResult
will contain the result of the addition 3. With first-class functions it is possible to store functions like other values in complex data structures or pass them deep down to other processes.
An easy example to understand be benefits would be two different operations executed on a list.
def sum(lst: [int]) -> int: if len(lst) == 0: return 0 result = lst[0] for i in range(1, len(lst)): result += lst[i] return result def product(lst: [int]) -> int: if len(lst) == 0: return 0 result = lst[0] for i in range(1, len(lst)): result *= lst[i] return result myList = [1, 2, 3] mySum = sum(myList) myProduct = product(myList)
We have two pure functions. sum
adds every element of the list to the result and returns it. product
also takes every element but multiplies it with the result. Both functions look nearly the same, just the operation differs. This can be written much cleaner with first-class functions.
def add(a: int, b: int) -> int: return a + b def mul(a: int, b: int) -> int: return a * b def foreach(lst: [int], op: Callable[[int, int], int]) -> int: if len(lst) <= 0: return 0 result = lst[0] for i in range(1, len(lst)): result = op(result, lst[i]) return result myList = [1, 2, 3] mySum = foreach(myList, add) myProduct = foreach(myList, mul)
At first glance this doesn’t safe much code, but we got rid of the code duplication. We have a clean separation between the array traversal and the operations. Even more important: if we want to add another operation like the division we just need to add 2 more lines of code.
Higher order functions
Higher order functions are functions that can deal with other functions, either as input arguments or as return values. In our previous example the foreach
is a higher order function because the parameter op
is expected to be a function.
Closures
Closures are functions that are aware of the scope they where declared in. If they are declared within a function for example they can utilize all of the variables and parameters of this function.
def add(a: int, b: int) -> int: return a+b def add_constant(const: int) -> Callable[[int], int]: def closure(x: int): return add(x, const) return closure addFive = add_constant(5) ten = addFive(5) eleven = addFive(6)
In this example we are using all of our concepts combined. First we have the add
function which is pure and does nothing more than add two parameters. There is also the add_constant
function which creates another function that will add a constant to any number. Because its return value is another function add_constant
is a higher order function. addFive
is the result of add_constant with the parameter 5 and for this reason a first class function.
Within add_constant
we can find our closure
. It will use the outer context of add_constant
when called and adds its argument to the variable const
which is declared in add_constant
. Because addFive
was created with const
being 5, everytime addFive
is called, 5 will be added to the input parameter.