Functional Programming

Learning goals

  • Understand what Functional Programming is and why it’s useful
  • Learn the common Functional Programming techniques in Python

Functional Programming is a style of programming in the same way as Object Oriented Programming is a style of programming. Writing great functional programs requires a slight mindset shift compared to the procedural (step-by-step) programming approach that comes naturally to most people, but it’s a powerful concept.

What is Functional Programming?

Functional Programming is a programming style where you think primarily in terms of functions, in the mathematical sense of the word.

A mathematical function is something that takes some inputs, and produces an output. Importantly that’s all it does – there are no side effects such as reading data from a web page, writing text to the console or modifying some shared data structure. In the absence of such side effects, the only thing that can influence the function’s behaviour is its inputs – so given a particular combination of inputs, we will always get back precisely the same outputs. A function that has these properties is called a pure function.

Functional programming is all about using pure functions as the building blocks of a program. Consider the following procedural (non-functional) piece of code:

input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list = []
for x in input:
    if x % 2 == 0:
        list.append(x)

output = list

This code takes an input array of integers, and produces an output array containing all the even numbers.

We call this code “procedural” because it defines a procedure for the computer to follow, step by step, to produce the result. We are talking to the computer at a detail level, specifying precisely which numbers to move around. We are also creating data structures that are modified as the program executes (the list).

Consider the functional alternative, which is to perform all this using pure functions:

input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
output = list(filter(lambda x: x % 2 == 0, input))

This version is shorter. But that is not its main selling point. The real benefit is that we’re talking at a much more abstract level about what we want the program to achieve, rather than what we want it to do step by step. You can read the second line of code fairly directly:

  • Take input
  • Apply a filter to it
    • The filter is “number is even”

Deep inside the computer’s CPU it will still be doing pretty much the same operations as the previous version, but we don’t need to know that – it’s a level of detail we don’t care about and it might not even be true.

The Benefits of Functional Programming

The example above is well representative of the benefits of functional programming. We can summarise these as follows:

  • Readability. By expressing our intentions at a higher level than in the procedural style, we can show our intent.
  • Conciseness. By the same token, we can implement the same functionality in fewer lines of code. Fewer lines means fewer bugs (certainly fewer characters you can mistype), so provided we retain readability conciseness is a good thing.
  • Intrinsic support for parallelism. Even if there’s no magic parallelism happening behind the scenes, functional programming is inherently well suited to multithreaded computing and carrying out different work strands in parallel. This is because its focus on pure functions means that we generally have immutable data structures – that is, data structures whose contents never change. As pure functions do not modify any state of the program, two functions never try to modify the same shared state concurrently, thus avoiding race conditions.

Immutablity

Data not changing doesn’t mean we can’t perform interesting computations. We can create new data structures which are different from the original ones, but we avoid modifying those that we have already created. In the examples above the input and output lists were never modified in either case, but the procedural code had a list variable that was modified, and the functional style eliminated this.

The Limitations of Functional Programming

Clearly, there are some spheres in which Functional Programming fits naturally and makes obvious sense. For instance, complex banking calculations will naturally fit the functional pattern because they are all about performing calculations on data. Even less obvious programs generally have elements which can be expressed more clearly and concisely in the functional style – a large proportion of for and foreach loops could be replaced with functional alternatives, as in the example above.

However, strict adherence to Functional Programming principles leads to challenges that make it harder to write many programs. For instance, if side effects are outlawed how can we read user input or write files to disk? Truly functional languages have workarounds for this that “hide” the side effect behaviour in a way that allows you to treat absolutely everything as an apparently pure function call.

The golden rule is as always: look for the programming style that lets you write a given bit of code in the simplest and most expressive manner possible. That’s often, but certainly not always, a functional style.

Iterables and iterators

An object is iterable if it allows you to step through the elements that it contains – think of anything that could be explored with a for loop. A list is an obvious example, but so are strings, dict objects (dictionaries that map from keys to values) and a number of other standard types.

An iterable object provides an iterator that gives access to the elements; the built-in function iter() returns an iterator for an object, so when you write for i in my_object you could instead write for i in iter(my_object). You can define your own custom iterators, but it’s unlikely that you’ll need to.

The main thing to know is that iterables and iterators are powerful tools for functional programming. Given an iterable containing a number of elements, it is relatively straightforward to:

  • perform some function on each of the elements – for example, given a list of numbers you want to produce a new list in which each of the numbers has been doubled
  • filter the elements to only keep those that match some rule – for example, from a list of words you only want to keep the words that include the letter ‘R’
  • perform some operation by processing all the elements – for example, max() is a standard function that returns the largest element in an iterable

List Comprehension

As mentioned earlier, in functional programming it is important that functions don’t have side effects and therefore the data objects should be immutable. List comprehension allows for a way to transform iterables, such as lists, into new lists.

The example:

input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
output = []

for x in input:
    output.append(x * 2)

can be turned into a simple list comprehension statement:

input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
output = [x * 2 for x in input]

As seen in this example, the pattern for list comprehension is [expression for item in iterable].

We can filter the input by adding a condition to the list comprehension statement, i.e. [x * 2 for x in input if x >= 5], which would only perform the expression(x * 2) on the item(x) if the condition (x >= 5) is satisfied – if the condition is not satisfied then the item would be skipped.

Side Effects

It is possible to create side effects with list comprehension, for instance using an append statement. This should be avoided.

Generator Expressions

Generator expressions are closely related to list comprehension. Consider the operation described earlier:

output = [x * 2 for x in input]

In cases where the input data list is very large or even infinite, we can’t compute the entire output list. Instead we can create a generator expression that enables us to compute the result for each input element as it is needed. A generator expression returns an iterator and is syntactically similar to a list comprehension statement – just use parenthese rather than square brackets as in:

output_generator = (x * 2 for x in input)

The iterator produced by a generator expression can itself be passed into a function.

The reduce function

The functools module provides the reduce function, which enables you to apply a function cumulatively across all the elements of an iterable. For example, you could implement your own version of the sum() function by writing the following.

import functools

def my_add(a, b):
    return a + b

my_sum = functools.reduce(my_add, [3, 24, 5])

The function that is passed into reduce() must take two parameters, and it is applied from left to right on the input. That is, the above statement would become:

my_sum = my_add(my_add(3, 24), 5)

Lambdas

Lambdas are small anonymous functions similar to JavaScript’s arrow functions. They provide a concise way to execute an expression on any number of arguments (unlike JavaScript’s arrow functions, lambdas can only execute one expression). Lambda functions follow the syntax lambda arguments : expression.

Lambda functions are used when a nameless function is needed. For instance, to double an input list, we can use a lambda expression in combination with map:

input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
output = list(map(lambda x: x * 2, input))

Here, the code iterates over the input list (map). We declare the lambda function with the word lambda, and define the name of the iterated item (x) and the expression we wish to perform (x * 2). We then pass in the input list (input). Finally, we turn the result to a list (wrapping in list()).

Assigning to variables

It is possible to assign a lambda to a variable – however this should be avoided. Doing so “eliminates the sole benefit a lambda expression can offer over an explicit def statement (i.e. that it can be embedded inside a larger expression)”.

See PEP 8 style guide for more information.

Further reading

Functional Programming HOWTO in the official Python documentation builds on the concepts introduced in this topic.

Other programming paradigms

A programming paradigm is a way of structuring and expressing the code that implements a software development solution. This module has been focused on the functional programming paradigm, and it should be clear how that differs from other ways of programming that you’re familiar with. There are many other programming paradigms, but the following are the most important at this stage of your learning.

  • Procedural
    • Code is structured as a set of procedures
    • This is a form of imperative programming, which means that the steps that the computer needs to follow are expressed in order
    • In procedural programming, you approach a problem by deciding how the necessary logical steps should be divided up
    • This used to be the dominant programming paradigm, before the widespread adoption of object-oriented programming
  • Object-oriented
    • Code is structured using classes and objects
    • As explored in the Object-Oriented Programming module, some of the strengths of this approach are:
      • Encapsulation of an object’s properties (data) and behaviour (methods) into a single entity
      • Abstraction, which means hiding details of an object’s implementation behind its interface
      • Inheritance of properties and methods from an ancestor class
      • Polymorphism, through which an object can be interacted with as if it were an instance of an ancestor class, while it behaves according to its actual class
    • In object-oriented programming, you approach a problem by dividing it up into entities (objects) that have particular properties and behaviour
    • This is a very popular programming paradigm for general-purpose software development, particularly as there are OO patterns for many common situations – e.g., MVC (Model-View-Controller) is an OO pattern for application development
  • Functional
    • As this module has explored, code is structured using functions
      • As much as possible, functions are pure and do not have side effects (input/output are exceptions to this)
      • Programs are made up of functions composed together so that the output of one is the input for another
      • This can make programs more readable
    • In functional programming, you approach a problem by breaking down the computations into stateless (pure) functions comprised of other functions
    • Functional programming is especially suited to complex manipulation of large amounts of data
  • Event driven
    • Code is structured as units that are executed in response to particular events
    • We will see some event driven programming in the Further JS: the DOM and Bundlers module, because the JavaScript code that runs in a web browser is executed in response to events (e.g., when a button is clicked or an API call responds)
    • In event driven programming, you approach a problem by identifying the events and defining the response to each one separately; the important concept to keep in mind is that the “flow” of the program logic is controlled by external events
    • This is particularly suited to game development and device drivers

It used to be the case that each programming language had a particular paradigm that needed to be followed. There are still some languages like that, but many modern languages support multiple paradigms – as this module and the Object-Oriented Programming module have shown.