10.3. Functional Purity

  • Pure functions have no side effects (i.e. memory, state, I/O)

  • Calling the pure function again with the same arguments returns the same result (this can enable caching optimizations such as memoization)

  • If the result of a pure expression is not used, it can be removed without affecting other expressions

  • If there is no data dependency between two pure expressions, their order can be reversed, or they can be performed in parallel and they cannot interfere with one another (the evaluation of any pure expression is thread-safe) [1]

Pure functions have two important properties [2]:

  • They always produce the same output with the same arguments irrespective of other factors. This property is also known as immutability.

  • They are deterministic. Pure functions either give some output or modify any argument or global variables i.e. they have no side effects.

Because pure functions have no side effects or hidden I/O, programs built using functional paradigms are easy to debug. Moreover, pure functions make writing concurrent applications easier.

When the code is written using the functional programming style, a capable compiler is able to:

  • Memorize the results

  • Parallelize the instructions

  • Wait for evaluating results

>>> def add(a, b):
...     return a + b

10.3.1. Pure Function

>>> def add(a, b):
...     return a + b
>>> c = 0
>>>
>>> add(1, 2)
3
>>>
>>> add(1, 2)
3
>>> c = 10
>>>
>>> add(1, 2)
3
>>>
>>> add(1, 2)
3

10.3.2. Impure Function

>>> def add(a, b):
...     return a + b + c
>>> c = 0
>>>
>>> add(1, 2)
3
>>>
>>> add(1, 2)
3
>>> c = 10
>>>
>>> add(1, 2)
13
>>>
>>> add(1, 2)
13

10.3.3. Impure to Pure Function

>>> def add(a, b, c):
...     return a + b + c
>>> c = 0
>>>
>>> add(1, 2, c)  # add(1,2,0)
3
>>>
>>> add(1, 2, c)  # add(1,2,0)
3
>>> c = 10
>>>
>>> add(1, 2, c)  # add(1,2,10)
13
>>>
>>> add(1, 2, c)  # add(1,2,10)
13

10.3.4. Side Effects

  • I/O - Input Output

  • Looks like a pure function

  • File content can change by other process

>>> def read(filename):
...     with open(filename) as file:
...         return file.read()

Each time when the function is called, the content of a file could be changed by some other process.

>>> with open('/tmp/myfile.txt', mode='wt') as file:
...     file.write('hello')
5
>>>
>>> read('/tmp/myfile.txt')
'hello'
>>>
>>> read('/tmp/myfile.txt')
'hello'
>>>
>>> read('/tmp/myfile.txt')
'hello'

Let's introduce modification to the file (done by some other code outside of the function scope). You can clearly see that the function is called with the same arguments as before, but this time it will give us some different results.

>>> with open('/tmp/myfile.txt', mode='wt') as file:
...     file.write('world')
5
>>>
>>> read('/tmp/myfile.txt')
'world'
>>>
>>> read('/tmp/myfile.txt')
'world'
>>>
>>> read('/tmp/myfile.txt')
'world'
../../_images/fp-purity-io.png

10.3.5. Bridge

  • Serves as a bridge between impure I/O and pure functional system

>>> def read(filename):  # impure bridge
...     with open(filename) as file:
...         return file.read()
>>>
>>> def strip(string):  # pure
...     return string.strip()
>>>
>>> def upper(string):  # pure
...     return string.upper()
>>>
>>> data = read('/tmp/myfile.txt')  # impure
>>> stripped = strip(data)     # pure
>>> result = upper(stripped)   # pure
>>>
>>> result
'WORLD'
../../_images/fp-purity-bridge.png

10.3.6. Use Case - 1

  • Math Functions

  • Mathematical functions are pure in general

>>> def add(a, b):
...     return a + b
>>> def odd(x):
...     return x % 2
>>> def cube(x):
...     return x ** 3

10.3.7. Use Case - 1

  • Select

Pure:

>>> DATA = [
...     (5.8, 2.7, 5.1, 1.9, 'virginica'),
...     (5.1, 3.5, 1.4, 0.2, 'setosa'),
...     (5.7, 2.8, 4.1, 1.3, 'versicolor'),
...     (6.3, 2.9, 5.6, 1.8, 'virginica'),
...     (6.4, 3.2, 4.5, 1.5, 'versicolor'),
...     (4.7, 3.2, 1.3, 0.2, 'setosa'),
... ]
>>>
>>>
>>> def function(data, species):
...     result = []
...     for *features, label in data:
...         if label == species:
...             result.append(features)
...     return result

Impure:

>>> DATA = [
...     (5.8, 2.7, 5.1, 1.9, 'virginica'),
...     (5.1, 3.5, 1.4, 0.2, 'setosa'),
...     (5.7, 2.8, 4.1, 1.3, 'versicolor'),
...     (6.3, 2.9, 5.6, 1.8, 'virginica'),
...     (6.4, 3.2, 4.5, 1.5, 'versicolor'),
...     (4.7, 3.2, 1.3, 0.2, 'setosa'),
... ]
>>>
>>>
>>> def function(species):
...     result = []
...     for *features, label in DATA:
...         if label == species:
...             result.append(features)
...     return result

10.3.8. References