This article is part of a series on functional programming.
Functional programming is one of several programming paradigms that Python supports.
In functional programming, functions are first class objects, and we use them as the basic building blocks of our programs (just as you might use objects to build your program if you are using object oriented programming).
Of course, in plain old procedural programming, we use functions to structure our code. The difference with functional programming if that function don't just operate on data, functions also operate on other functions. That is what we mean by functions being first class objects - you can store a function object in a variable, pass a function into a function, even return a function from a function, exactly as you could with a number, string or list.
Functional and object oriented programming both aim to simplify complex programs by using abstractions. OOP works well in scenarios where the software is structurally complex because it organises the program's data into self contained units that interact in controlled ways. Functional programming works well with software that is algorithmically complex - for example "artificial intelligence" type applications. Python allows you to mix both styles.
Just like OOP, there are no exact rules that you have to follow to create functional code. But there are certain characteristics and patterns functional programming tends to follow:
You will also see lambda functions, closures, partials and currying being used. We will look at some of those aspects in the rest of this article.
Quite often when you call a function it will have some lasting effect on the system - a side effect. Examples are:
We often need side effects (programs sometimes have to write data to disk, for example), but they can make the behaviour of a program quite unpredictable. In functional programming we try to limit side effects by using pure functions (functions with no side effects) wherever possible. In particular, if a system involves complex algorithms, we might try to implement these in a subsystem that uses the functional programming paradigm. Elements such as disk i/o or user interfaces can then be implemented separately.
Here is an example of a pure function:
def add(a, b): return a + b
Calling this function has an entirely predictable effect. It return the sum of
b, and has absolutely no other side effect.
If we build our code entirely out of pure functions, the entire behaviour is predictable.
In theory we could even mathematically prove that the function works correctly. In practice, any useful program is usually too complex to be proved correct in this way, but functional code is still easier to test because there are no external factors to worry about.
Procedural programs often work directly on lists, arrays and other data structures. A potential problem with that is that your program might change the data, deliberately or even accidentally. Changing the data causes a side effect, which we are trying to avoid.
One way to avoid this is to use iterators. You can obtain an iterator from
a list (or any iterable item), using the
iter function. Once you have an iterator, you can read the elements of the list one by one,
k = [10, 20, 30, 40] it = iter(k) print(next(it)) # 10 print(next(it)) # 20 print(next(it)) # 30 print(next(it)) # 40
The advantage of this is that an iterator only allows you to read elements. If you pass an iterator into a function, rather than the list itself, the function can't change the list.
Another advantage of iterators is that they are lazy - they will only request the next element when they need it. This can be very useful when processing long sequences.
Higher order functions are functions that act on other functions. That doesn't mean functions that call other functions, it means functional that accept functions as parameters, or have a function as their return value.
As this is quite key to the idea of functional programming, as you might imagine there are quite a few built-in higher order functions, and it is also easy to write your own.
As a simple example, the map function accepts a function and a sequence of values as parameters. It applies the function to each item in the sequence, and returns the result as an iterator. For example:
def add3(x): return x+3 it = map(add3, [1, 2, 3, 4]) print(list(it)) # [4, 5, 6, 7]
We have defined a simple function that adds 3 to its input. We then use
map to apply that function to the list
[1, 2, 3, 4] giving the
[4, 5, 6, 7].
Now, clearly, we could have done the same thing with a simple loop. Why bother with
map? Well, it hides the unimportant implementation detail
(the loop) and lets you concentrate on what really matters (the function is being applied to the list of values). This means that if you
need to do more complicated things, maybe with several higher order functions, you don't get bogged down in the details.
Another advantage is that it automatically supports lazy iteration. When you call the
map function, it doesn't actually do any calculations.
It simply returns a special iterator (a
map object) that knows what to do when it is asked to. No matter how long the list,
return very quickly.
As you can see from the code, we can't just print
it (the result of the call to
map) - well we could, but the result would be pretty boring,
it is just a
map object. To see the actual values we must convert
it into a list. What the
list function does is to continuously call
next on the
map object, and store each value in the list. This is when the actual calculations are done.
If you found this article useful you might be interested in my ebook Functional Programming in Python.
Copyright (c) Axlesoft Ltd 2020