Functional Programming illustrated in Python: Part 0

Introduction

Brian Candler
7 min readOct 21, 2020

Welcome to a short series of articles which introduce functional programming concepts in a simple, easy-to-digest and non-mathematical way.

I have chosen Python because it’s familiar to many people. Whilst not generally considered a “functional” language, it has the tools needed to show the concepts clearly. It’s also comforting — it doesn’t have lots of brackets like Lisp, or almost no brackets at all like Haskell, and it has a good old print() function!

Road map

… more to come (maybe)

Pre-requisites

I am going to assume you know a bit of basic Python. You know how to run a Python script, and run Python interactively. There will be lots of small examples that you can run yourself.

I’m also going to assume you’re familiar with the concepts of “class”, “object” and “method”. To recap, a “class” is a type of object. Some are built-in, and others you define yourself:

class Point:
def __init__(self, x, y):
self.x = x
self.y = y

You can then create objects (also known as “instances” of the class), by calling the class’s constructor:

p1 = Point(3, 4)
p2 = Point(12, 13)

“Methods” are functions defined in the class which are available to instances.

import mathclass Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance_from_origin(self):
return math.sqrt(self.x ** 2 + self.y ** 2)
>>> p1 = Point(3, 4)
>>> p1.distance_from_origin()
5.0

If all that is familiar to you, you’ll be just fine.

What is a function anyway?

If you’ve written Python before, you’ll have an idea of what a function is. A function definition looks like this:

def say_hello():
print("Hello!")

It’s some code that you write, and when you call the function, the Python interpreter runs the code. If you are following along interactively, where >>> is the prompt from the Python interpreter, then you can call it like this:

>>> say_hello()
Hello!

Functions can have one or more parameters, which are set to values supplied by the caller (arguments) when it is called:

def greet(name):
print("Hello, " + name + "!")
>>> greet("Brian")
Hello, Brian!

A function can also return a value.

def make_greeting(name):
return "Hello, " + name + "!"
>>> result = make_greeting("Brian")
>>> print(result)
Hello, Brian!

In Python, if you don’t return a value then the function implicitly returns the special value None. Python doesn’t check the types of the arguments, nor the return value; they can be anything you like.

Functions as values

In Python, a function is a value, just like the number 1 is a value. What does that mean? You can’t do arithmetic on it:

>>> say_hello + 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'function' and 'int'

You can print the value, although what you see is not very useful:

>>> print(say_hello)
<function say_hello at 0x7f1965716310>

But you can usefully assign it to a variable:

>>> bonjour = say_hello
>>> bonjour()
Hello!

You can also pass these values to another function. That other function can then in turn call the function (or not) as it chooses. For example:

def do_it_twice(something):
something()
something()
>>> do_it_twice(say_hello)
Hello!
Hello!

The function do_it_twice encapsulates the idea of doing something twice. If we pass in say_hello then that’s what it does twice.

A function can also return a function value, just as it would return any other value. Suppose you have todo-list, and it returns some action that you need to do later:

def todo_list_action():
return say_hello
>>> action = todo_list_action()
>>> action()
Hello!

This ability to pass and return functions means that functions are first class values in Python. Functions which take or return functions are called higher order functions.

Anonymous Functions

Naming functions can sometimes be clumsy. Here’s a function which wants to build and return another function:

def make_action(item):
def myfunc():
print("It's time to make the " + item)
return myfunc
>>> action = make_action("tea")
>>> action()
It's time to make the tea

Inside make_action I wanted to define and return a function which prints a reminder to do something. I had to define this function with some random name, like “myfunc”, just so that I could return it — but that name has no significance to the receiver of the function.

There’s a better way to do this. lambda lets you create a function value without giving it a name¹. The code simplifies to:

def make_action(item):
return lambda: print("It's time to make the " + item)

You need to be very clear about the difference between the following two expressions. The expression

print("It's time to make the " + item)

will print something right now when it is executed. The expression

lambda: print("It's time to make the " + item)

will create a function value which lets you do something later. If you don’t make use of this value, then nothing will happen.

In essence, def is just creating a lambda and assigning it to a variable². The following two definitions are identical:

def say_hello1():
print("Hello!")
say_hello2 = lambda: print("Hello!")>>> say_hello1()
Hello!
>>> say_hello2()
Hello!

Lambdas can have parameters — they go before the colon — and the return value is the value of the expression which forms the lambda body (you don’t use the “return” keyword)

>>> add = lambda x, y: x+y
>>> add(2,3)
5

You can create a lambda and call it immediately:

>>> (lambda x, y: x+y)(2,3)
5

A lambda can return a lambda, like a factory:

make_action = lambda item: lambda: print("Make the " + item)

You might find that clearer split over multiple lines, so it looks more like the def form:

make_action = (lambda item:
lambda: print("Make the " + item)
)

Whichever way you write it, it runs the same:

>>> action = make_action("tea")
>>> action()
Make the tea

Pure Functions

Most of the things we’ve seen so far are not, strictly speaking, functions. They would be better called “procedures” or “subroutines” — although in Python there’s no distinction.

Pure functions have the following characteristics³:

  • The return value depends only on the values of the arguments provided. The behaviour of the function does not depend on any other state of the system, such as state from previous invocations, terminal input, or the current time.
  • They do not alter the state of the system in any way.

say_hello is not a pure function because it has side-effects — it causes output to appear on the terminal. As a result, it’s also not idempotent: this means that calling it multiple times has a different effect than calling it once (as we demonstrated with do_it_twice). The same applies to print itself. Any function which uses print is not pure.

But make_greeting is a pure function. The value returned depends only on the arguments, and it has no side-effects.

Clearly, a pure function must have at least one parameter. A pure function with no parameters would return the same value every time it is called, so would just be a constant.

Calling a function, such as make_greeting("Brian"), transforms one or more values into another value. This is also called applying a function to a value (or values).

The utility of pure functions

Pure functions don’t “do” anything, apart from calculating a return value from its arguments. If you come from a procedural computing background, that might not sound very useful.

They do have the obvious benefit that they are incredibly easy to reason about, precisely because they are so limited: you don’t have to take into account any other state in the system. If you are writing any large system, any parts which you can reformulate as pure functions are easy to develop and test.

However, the fascinating fact is that anything you can do on a computer, you can do with just pure functions and nothing else— something which was proved long ago.

The proof goes something like this: any program that’s worth running can be run on a Commodore 64. Therefore, if you can write a Commodore 64 emulator using just lambdas, then you can run anything useful with just lambdas. (The mathematical proof uses an abstract machine called a Turing Machine, but that is really just a Commodore 64 with unlimited amounts of memory, or a big stack of 5¼" floppy disks)

Writing programs using only pure functions can require a radically different way of thinking, combining them in ways which might not be obvious at first. When you add in a strong type system it can result in extremely robust software. Even partitioning your software into pure-parts and non-pure parts can yield big benefits.

There are of course many, many other books and articles on functional programming. This series attempts to demystify their terminology in terms that any Python programmer can understand, and therefore may be most useful when read alongside one of those other works.

[1] Why is it calledlambda? Lambda is the Greek letter λ. Can you find it on your keyboard? Me neither! But this is the symbol used in the “Lambda Calculus”, which is the formal mathematical analysis of functions as values, as the way of defining a function. Haskell uses \ as the symbol, because it looks a bit like a Lambda if you squint. e.g. \x -> x * x instead of lambda x: x*x

[2] Actually, Python only lets you put an expression as the body of a lambda, which is a subset of the full language — you can’t include an assignment or “for” loop for example. So sometimes you’re forced to use the “def” form.

[3] Read also referential transparency.

--

--

No responses yet