Functional Programming

Programming paradigms

In connection with functional programming (see also wikipedia) we start with a few words about how do we decompose a program, that is about the programming paradigms. There are some main types of them, but several programming languages cannot be strictly classified into one paradigm, but rather include features from several paradigms, like Python.

  • Imperative in which you instruct the machine what to do, how to change its state.
    • Procedural where your programs are lists of instructions which are grouped into procedures (e.g. C, FORTRAN, bash).
    • Object-oriented where you manipulate objects having internal state which can be queried or modified by methods. (e.g. Java)
  • Declarative where you describe the problem, and the program figures out how to compute it.
    • Logic which mainly based on formal logic. (e.g. Prolog)
    • Functional in which the desired result is declared as the value of a series of function calls. (e.g. Haskell, Standard ML, Lisp)

In the functional paradigm any function $f(x)$ results the same value for the same $x$.

This is not the case if you have variables and change them while the program is running. Look at the following function (result depends on $x$).

In [1]:
def f(y):
    return x + y

x = 1
print(f(5))
x = -5
print(f(5))
6
0

If you avoid those, then the python functions will be actual mathematical functions), otherwise those are sequence of commands. Hence the name functional programming.

History

Originally, Guido van Rossum wanted to drop out the main functional programming tools from the core of Python 3 into a modul, because python has list comprehension instead. But after a strong lobby of functional programming fanatics Rossum kept the lambda, map() and filter() in the core and moved reduce() to the functools package.

Advantages

The theoretical and practical advantages to the functional style are

  • formal provability of the correcness of code,
  • easier debugging and testing as there are no states, so no side effects, no mutable objects,
  • parallel programmability, modularity, composability.

List comprehension and generator expression

The first and most important functional programming element of Python is the iterator what we introduced before. Running through the elements of an iterable object and performing an operation on them or selecting some of them by a condition are the most important tasks in a functional programming approach. In Python one may use list comprehension or generator expression for these tasks. List comprehension gives back a list, generator expression an iterator. The only difference is that we put the former in [] and the latter in ().

After reading the lines from a file into a list of strings, remove the white space characters from the beginning and end of each line, using the .strip() method.

In [2]:
lines = ['  line1 ', 'line2\t', '', ' line3 \n ']
stripped_iter = (line.strip() for line in lines)
In [3]:
for i in stripped_iter:
    print(i, end="")
line1line2line3
In [4]:
[line.strip() for line in lines]
Out[4]:
['line1', 'line2', '', 'line3']
In [5]:
[line.strip() for line in lines if line != ""]
Out[5]:
['line1', 'line2', 'line3']

The general form of the generator expression:

(expression for expr1 in seq1
            if condition1
            for expr2 in seq2
            if condition2
                ...
            for exprN in seqN
            if conditionN )

The list comprehension is the same except that [] is used instead of ().

These are equivalent to the next code. Mind the position of the main expression:

for expr1 in seq1:
    if not (condition1):
        continue
    for expr2 in seq2:
        if not (condition2):
            continue 
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue
            expression
In [6]:
[(x, y, x * y) for x in (1, 2, 3) for y in [1, 2, 3, 4] if x < y]
Out[6]:
[(1, 2, 2), (1, 3, 3), (1, 4, 4), (2, 3, 6), (2, 4, 8), (3, 4, 12)]
In [7]:
for x in (1, 2, 3):
    for y in [1, 2, 3, 4]:
        if x < y:
            print((x, y, x*y), end=", ")
(1, 2, 2), (1, 3, 3), (1, 4, 4), (2, 3, 6), (2, 4, 8), (3, 4, 12), 

Print out the leap years between 1890 and 1914!

In [8]:
[year for year in range(1890, 1915)];
In [9]:
[year for year in range(1890, 1915) if year%4 == 0]
Out[9]:
[1892, 1896, 1900, 1904, 1908, 1912]
In [10]:
[year for year in range(1890, 1915) 
    if (year%4 == 0 and year%100 != 0) or year%400 == 0]
Out[10]:
[1892, 1896, 1904, 1908, 1912]

Lambda functions

Some simple functions can be written shorter with lambda. Traditional function, the form of it is

lambda arguments: expression
In [11]:
def square(x):
    return x * x

f = square
f(5)
Out[11]:
25

Instead you can make a function object and call it with paranthesis, like you would call a function.

In [12]:
square = lambda x: x*x
square(6)
Out[12]:
36
In [13]:
(lambda x: x**2)(6)
Out[13]:
36

A lambda function can have more arguments:

In [14]:
expr = lambda a, b, c: a + b*c
expr(2, 4, 4)
Out[14]:
18

map()

The map applies a given function for every element of an iterable object and returns an iterator. Describing with a math formula: $$\mathop{\mathrm{map}}:(A\mapsto B)\times A^n\mapsto B^n\ \ (n\in \mathbb{N})$$

In [15]:
l = [1, 5, 8]
list(map(square, l))
Out[15]:
[1, 25, 64]

The same with lambda (and list comprehension):

In [16]:
list(map(lambda x: x**2, range(5)))
Out[16]:
[0, 1, 4, 9, 16]
In [17]:
[x**2 for x in range(5)]
Out[17]:
[0, 1, 4, 9, 16]

You can use a multi-variate function as the first argument. If $f$ has more variable, then you have to provide more iterables.

$$\mathop{\mathrm{map}}: (A \mapsto X)\times A^n \mapsto X^n$$$$\mathop{\mathrm{map}}: (A\times B \mapsto X) \times A^n\times B^n \mapsto X^n$$$$\mathop{\mathrm{map}}: (A\times B \times C\mapsto X)\times A^n\times B^n\times C^n \mapsto X^n$$$$\vdots$$
In [18]:
map(pow, [0.5, 1, 1.5], [-1, 2, 3])
Out[18]:
<map at 0x7f45fc423cc0>
In [19]:
list(map(pow, [0.5, 1, 1.5], [-1, 2, 3]))
Out[19]:
[2.0, 1, 3.375]
In [20]:
list(map(pow, [0.5, 1], [-1, 2, 3]))
Out[20]:
[2.0, 1]

Conditional expressions

We may use conditional expressions in lambda functions. Let us recall that the general form of a conditional expression is

expression if condition else expression when condition false
In [21]:
x = -4
x if x>0 else -x
Out[21]:
4
In [22]:
absolute = lambda x: x if x >= 0 else -x

print(absolute(5), absolute(-5))
5 5
In [23]:
list(map(lambda x: x if x>=0 else 0, range(-5,5)))
Out[23]:
[0, 0, 0, 0, 0, 0, 1, 2, 3, 4]

One of the advantage of using conditional expressions is that both if and else are necessary, which makes it harder to forget a case.

Bad example:

In [24]:
def ageroup(x):
    if x < 8:
        return "child"
    elif x < 15:
        return "adolescent"
    elif x >=18:
        return "adult"

print(ageroup(16))
None
In [25]:
def ageroup(x):
    return "child" if x<8 else ("adolescent"
            if x<15 else ("adult" if x>=18 else "INVALID"))

ageroup(16)
Out[25]:
'INVALID'

filter()

The filter makes an iterator from an iterable object according to a condition given as a logical function.

The logical function should be $A\mapsto \{True, False\}$.

$$\mathrm{filter} : (A\mapsto \{True, False\})\times A^n\mapsto A^m \text{ ahol } m\leq n$$
In [26]:
list(filter(lambda x: x % 2 == 0, range(10)))
Out[26]:
[0, 2, 4, 6, 8]
In [27]:
[x for x in range(10) if x % 2 == 0]
Out[27]:
[0, 2, 4, 6, 8]
In [28]:
list(filter(str.isupper, "aAbBcC"))
Out[28]:
['A', 'B', 'C']

zip()

In [29]:
list(zip(['a', 'b', 'c'], (1, 2, 3)))
Out[29]:
[('a', 1), ('b', 2), ('c', 3)]

any() és all()

According to the quantifiers the any() and all() built-ins look at the truth values of an iterable object, and any() returns True if any element in the iterable is true, and all() returns True if all of the elements are true.

In [30]:
any([1,2,0,1])
Out[30]:
True
In [31]:
all([1,2,0,1])
Out[31]:
False
In [32]:
import math
print([math.cos(i*math.pi) for i in range(3)])
any([math.cos(i*math.pi) == 1.0 for i in range(3)])
[1.0, -1.0, 1.0]
Out[32]:
True
In [33]:
all([math.cos(i*math.pi) == 1.0 for i in range(3)])
Out[33]:
False

reduce()

The functools.reduce(*function*, *iterable*[, *initial*]) cumulatively performs a bi-variate function on all the iterable’s elements. functools.reduce() takes the first two elements a and b returned by the iterator and calculates func(a, b), then calculates func(func(a, b), c) with the third element, and continues until the iterable is exhausted. If an initial value is given, then the calculation starts with func(initial, a). If only one element is offered by the iterator, then that will be the result. If no element is offered by the iteratot then a TypeError exception is raised. So always use an initial value if this may happen.

In [34]:
from functools import reduce
addition = lambda a, b: a+b
reduce(addition, [1, 2, 3, 4])
Out[34]:
10
In [35]:
reduce(addition, [4])
Out[35]:
4
In [36]:
reduce(addition, [], 0)
Out[36]:
0
In [37]:
division = lambda a, b: a/b
reduce(division, [2, 3, 4], 1)
Out[37]:
0.041666666666666664
In [38]:
1/24
Out[38]:
0.041666666666666664

The paradigm of accumulation can be done with this. Finding the extrema can be one application.

In [39]:
greater = lambda x, y: x if x > y else y
reduce(greater, [0, 1, 10, -10, 2], float("-inf"))
Out[39]:
10
In [40]:
smaller = lambda x, y: x if x < y else y
reduce(smaller, [0, 1, 10, -10, 2], float("inf"))
Out[40]:
-10

Two examples

Generalized inner product

The dot product can be implemented functionally:

In [41]:
def dot(a, b):
    return sum(map(lambda x, y: x*y, a, b))

dot([0.5, 1, 1.5], [-1, 1, -1])
Out[41]:
-1.0

In general, expressions of the form $f(g(x_1, y_1), g(x_2, y_2), \ldots g(x_n, y_n))$ can be considered a generalized inner product.

It is easy to implement functionally:

In [42]:
def inner(a, b, g=lambda x, y: x*y, f=sum):
    return f(map(g, a, b))

inner([0.5, 1, 1.5], [-1, 1, -1])
Out[42]:
-1.0
In [43]:
inner("abc", [1,3,2], f=list)
Out[43]:
['a', 'bbb', 'cc']

Or with a bi-variate, asssociative $g$ function via a reduce:

In [44]:
def inner(a, b, g=lambda x, y: x*y, f=lambda a, b: a+b, i=0):
    return reduce(f, map(g, a, b), i)

inner([0.5, 1, 1.5], [-1, 1, -1])
Out[44]:
-1.0
In [45]:
inner("abc", [1,3,2], i='')
Out[45]:
'abbbcc'
In [46]:
inner([1,11,4], [3,2,6], g=min, f=max, i=float("-Inf"))
Out[46]:
4
In [47]:
inner([1,11,4], [3,2,6], g=max, f=min, i=float("Inf"))
Out[47]:
3

Or do something similar with the 'or's of 'and's or with the 'and's of 'or's. For this we need a function version of the operations or and and. We may construct these functions or we may use the operations modul:

In [48]:
or_ = lambda a, b: a or b
and_ = lambda a, b: a and b
In [49]:
from operator import or_, and_
In [50]:
inner([0,0,1,1], [0,1,0,1], g=or_, f=and_, i=1)
Out[50]:
0
In [51]:
inner([0,0,1,1], [0,1,0,1], g=and_, f=or_, i=0)
Out[51]:
1

The eight queens puzzle

The eight queens puzzle is the problem of placing eight chess queens on an $8\times8$ chessboard so that no two queens threaten each other. A solution requires that no two queens share the same row, column, or diagonal. How many solutions are there? List them!

Two queens in the line $i$ and $j$ ($i\neq j$) does not threaten each other if $\mathrm{abs}(i - j)\neq\mathrm{abs}(x[i] - x[j])$. Listing the permutations we import the function permutations from the itertools module. This iterates through the permutations of the elements.

In [52]:
from itertools import permutations
s = 0
for x in permutations(list(range(8))):
    if all([i == j or abs(i - j) != abs(x[i] - x[j])
            for i in range(8) for j in range(8)]):
        s += 1
print(s)
92
In [53]:
for x in permutations(list(range(8))):
    if all([i == j or abs(i - j) != abs(x[i] - x[j])
            for i in range(8) for j in range(8)]):
        print(x)
(0, 4, 7, 5, 2, 6, 1, 3)
(0, 5, 7, 2, 6, 3, 1, 4)
(0, 6, 3, 5, 7, 1, 4, 2)
(0, 6, 4, 7, 1, 3, 5, 2)
(1, 3, 5, 7, 2, 0, 6, 4)
(1, 4, 6, 0, 2, 7, 5, 3)
(1, 4, 6, 3, 0, 7, 5, 2)
(1, 5, 0, 6, 3, 7, 2, 4)
(1, 5, 7, 2, 0, 3, 6, 4)
(1, 6, 2, 5, 7, 4, 0, 3)
(1, 6, 4, 7, 0, 3, 5, 2)
(1, 7, 5, 0, 2, 4, 6, 3)
(2, 0, 6, 4, 7, 1, 3, 5)
(2, 4, 1, 7, 0, 6, 3, 5)
(2, 4, 1, 7, 5, 3, 6, 0)
(2, 4, 6, 0, 3, 1, 7, 5)
(2, 4, 7, 3, 0, 6, 1, 5)
(2, 5, 1, 4, 7, 0, 6, 3)
(2, 5, 1, 6, 0, 3, 7, 4)
(2, 5, 1, 6, 4, 0, 7, 3)
(2, 5, 3, 0, 7, 4, 6, 1)
(2, 5, 3, 1, 7, 4, 6, 0)
(2, 5, 7, 0, 3, 6, 4, 1)
(2, 5, 7, 0, 4, 6, 1, 3)
(2, 5, 7, 1, 3, 0, 6, 4)
(2, 6, 1, 7, 4, 0, 3, 5)
(2, 6, 1, 7, 5, 3, 0, 4)
(2, 7, 3, 6, 0, 5, 1, 4)
(3, 0, 4, 7, 1, 6, 2, 5)
(3, 0, 4, 7, 5, 2, 6, 1)
(3, 1, 4, 7, 5, 0, 2, 6)
(3, 1, 6, 2, 5, 7, 0, 4)
(3, 1, 6, 2, 5, 7, 4, 0)
(3, 1, 6, 4, 0, 7, 5, 2)
(3, 1, 7, 4, 6, 0, 2, 5)
(3, 1, 7, 5, 0, 2, 4, 6)
(3, 5, 0, 4, 1, 7, 2, 6)
(3, 5, 7, 1, 6, 0, 2, 4)
(3, 5, 7, 2, 0, 6, 4, 1)
(3, 6, 0, 7, 4, 1, 5, 2)
(3, 6, 2, 7, 1, 4, 0, 5)
(3, 6, 4, 1, 5, 0, 2, 7)
(3, 6, 4, 2, 0, 5, 7, 1)
(3, 7, 0, 2, 5, 1, 6, 4)
(3, 7, 0, 4, 6, 1, 5, 2)
(3, 7, 4, 2, 0, 6, 1, 5)
(4, 0, 3, 5, 7, 1, 6, 2)
(4, 0, 7, 3, 1, 6, 2, 5)
(4, 0, 7, 5, 2, 6, 1, 3)
(4, 1, 3, 5, 7, 2, 0, 6)
(4, 1, 3, 6, 2, 7, 5, 0)
(4, 1, 5, 0, 6, 3, 7, 2)
(4, 1, 7, 0, 3, 6, 2, 5)
(4, 2, 0, 5, 7, 1, 3, 6)
(4, 2, 0, 6, 1, 7, 5, 3)
(4, 2, 7, 3, 6, 0, 5, 1)
(4, 6, 0, 2, 7, 5, 3, 1)
(4, 6, 0, 3, 1, 7, 5, 2)
(4, 6, 1, 3, 7, 0, 2, 5)
(4, 6, 1, 5, 2, 0, 3, 7)
(4, 6, 1, 5, 2, 0, 7, 3)
(4, 6, 3, 0, 2, 7, 5, 1)
(4, 7, 3, 0, 2, 5, 1, 6)
(4, 7, 3, 0, 6, 1, 5, 2)
(5, 0, 4, 1, 7, 2, 6, 3)
(5, 1, 6, 0, 2, 4, 7, 3)
(5, 1, 6, 0, 3, 7, 4, 2)
(5, 2, 0, 6, 4, 7, 1, 3)
(5, 2, 0, 7, 3, 1, 6, 4)
(5, 2, 0, 7, 4, 1, 3, 6)
(5, 2, 4, 6, 0, 3, 1, 7)
(5, 2, 4, 7, 0, 3, 1, 6)
(5, 2, 6, 1, 3, 7, 0, 4)
(5, 2, 6, 1, 7, 4, 0, 3)
(5, 2, 6, 3, 0, 7, 1, 4)
(5, 3, 0, 4, 7, 1, 6, 2)
(5, 3, 1, 7, 4, 6, 0, 2)
(5, 3, 6, 0, 2, 4, 1, 7)
(5, 3, 6, 0, 7, 1, 4, 2)
(5, 7, 1, 3, 0, 6, 4, 2)
(6, 0, 2, 7, 5, 3, 1, 4)
(6, 1, 3, 0, 7, 4, 2, 5)
(6, 1, 5, 2, 0, 3, 7, 4)
(6, 2, 0, 5, 7, 4, 1, 3)
(6, 2, 7, 1, 4, 0, 5, 3)
(6, 3, 1, 4, 7, 0, 2, 5)
(6, 3, 1, 7, 5, 0, 2, 4)
(6, 4, 2, 0, 5, 7, 1, 3)
(7, 1, 3, 0, 6, 4, 2, 5)
(7, 1, 4, 2, 0, 6, 3, 5)
(7, 2, 0, 5, 1, 4, 6, 3)
(7, 3, 0, 2, 5, 1, 6, 4)
In [ ]: