Many functional programming articles teach abstract functional techniques. That is, composition, pipelining, higher order functions. This one is different. It shows examples of imperative, unfunctional code that people write every day and translates these examples to a functional style.
The first section of the article takes short, data transforming loops and translates them into functional maps and reduces. The second section takes longer loops, breaks them up into units and makes each unit functional. The third section takes a loop that is a long series of successive data transformations and decomposes it into a functional pipeline.
The examples are in Python, because many people find Python easy to read. A number of the examples eschew pythonicity in order to demonstrate functional techniques common to many languages: map, reduce, pipeline.
When people talk about functional programming, they mention a dizzying number of “functional” characteristics. They mention immutable data, first class functions and tail call optimisation. These are language features that aid functional programming. They mention mapping, reducing, pipelining, recursing, currying and the use of higher order functions. These are programming techniques used to write functional code. They mention parallelization, lazy evaluation and determinism. These are advantageous properties of functional programs.
Ignore all that. Functional code is characterised by one thing: the absence of side effects. It doesn’t rely on data outside the current function, and it doesn’t change data that exists outside the current function. Every other “functional” thing can be derived from this property. Use it as a guide rope as you learn.
This is an unfunctional function:
In [2]:
a = 0
def increment1():
global a
a += 1
This is a functional function:
In [3]:
def increment2(a):
return a + 1
This is a simple map that takes a list of names and returns a list of the lengths of those names:
In [4]:
name_lengths = map(len, ["Mary", "Isla", "Sam"])
for i in name_lengths:
print(i)
This is a map that squares every number in the passed collection:
In [5]:
squares = map(lambda x: x * x, [0, 1, 2, 3, 4])
for i in squares:
print(i)
This map doesn’t take a named function. It takes an anonymous, inlined function defined with lambda. The parameters of the lambda are defined to the left of the colon. The function body is defined to the right of the colon. The result of running the function body is (implicitly) returned.
The unfunctional code below takes a list of real names and replaces them with randomly assigned code names.
In [6]:
import random
names = ['Mary', 'Isla', 'Sam']
code_names = ['Mr. Pink', 'Mr. Orange', 'Mr. Blonde']
for i in range(len(names)):
names[i] = random.choice(code_names)
print(names)
(As you can see, this algorithm can potentially assign the same secret code name to multiple secret agents. Hopefully, this won’t be a source of confusion during the secret mission.)
In [7]:
import random
names = ['Mary', 'Isla', 'Sam']
secret_names = map(lambda x: random.choice(['Mr. Pink',
'Mr. Orange',
'Mr. Blonde']),
names)
In [8]:
for i in secret_names:
print(i)
Exercise 1. Try rewriting the code below as a map. It takes a list of real names and replaces them with code names produced using a more robust strategy.
In [9]:
names = ['Mary', 'Isla', 'Sam']
for i in range(len(names)):
names[i] = hash(names[i])
print(names)
(Hopefully, the secret agents will have good memories and won’t forget each other’s secret code names during the secret mission.)
My solution:
In [10]:
names = ['Mary', 'Isla', 'Sam']
secret_names = map(hash, names)
In [11]:
for i in secret_names:
print(i)
In [15]:
from functools import reduce #Since Python 3
mysum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4])
print(mysum)
x
is the current item being iterated over. a is the accumulator. It is the value returned by the execution of the lambda on the previous item. reduce()
walks through the items. For each one, it runs the lambda on the current a
and x
and returns the result as the a
of the next iteration.
What is a
in the first iteration? There is no previous iteration result for it to pass along. reduce()
uses the first item in the collection for a
in the first iteration and starts iterating at the second item. That is, the first x
is the second item.
This code counts how often the word 'Sam' appears in a list of strings:
In [16]:
sentences = ['Mary read a story to Sam and Isla.',
'Isla cuddled Sam.',
'Sam chortled.']
sam_count1 = 0
for sentence in sentences:
sam_count1 += sentence.count('Sam')
print(sam_count1)
In [19]:
sentences = ['Mary read a story to Sam and Isla.',
'Isla cuddled Sam.',
'Sam chortled.']
sam_count2 = reduce(lambda a, x: a + x.count('Sam'),
sentences,
0)
print(sam_count2)
How does this code come up with its initial a
? The starting point for the number of incidences of 'Sam'
cannot be 'Mary read a story to Sam and Isla.' The initial accumulator is specified with the third argument to reduce()
. This allows the use of a value of a different type from the items in the collection.
Why are map and reduce better?
First, they are often one-liners.
Second, the important parts of the iteration - the collection, the operation and the return value - are always in the same places in every map and reduce.
Third, the code in a loop may affect variables defined before it or code that runs after it. By convention, maps and reduces are functional.
Fourth, map and reduce are elemental operations. Every time a person reads a for
loop, they have to work through the logic line by line. There are few structural regularities they can use to create a scaffolding on which to hang their understanding of the code. In contrast, map and reduce are at once building blocks that can be combined into complex algorithms, and elements that the code reader can instantly understand and abstract in their mind. “Ah, this code is transforming each item in this collection. It’s throwing some of the transformations away. It’s combining the remainder into a single output.”
Fifth, map and reduce have many friends that provide useful, tweaked versions of their basic behaviour. For example: filter
, all
, any
and find
.
Exercise 2. Try rewriting the code below using map, reduce and filter. Filter takes a function and a collection. It returns a collection of every item for which the function returned True.
In [20]:
people = [{'name': 'Mary', 'height': 160},
{'name': 'Isla', 'height': 80},
{'name': 'Sam'}]
height_total = 0
height_count = 0
for person in people:
if 'height' in person:
height_total += person['height']
height_count += 1
if height_count > 0:
average_height = height_total / height_count
print(average_height)
If this seems tricky, try not thinking about the operations on the data. Think of the states the data will go through, from the list of people dictionaries to the average height. Don’t try and bundle multiple transformations together. Put each on a separate line and assign the result to a descriptively-named variable. Once the code works, condense it.
In [30]:
people = [{'name': 'Mary', 'height': 160},
{'name': 'Isla', 'height': 80},
{'name': 'Sam'}]
heights = map(lambda x: x['height'],
filter(lambda x: 'height' in x, people))
heightsList = list(heights) # Neccesary in Python3
if len(heightsList) > 0:
from operator import add
average_height = reduce(add, heightsList) / len(heightsList)
print(average_height)
The program below runs a race between three cars. At each time step, each car may move forwards or it may stall. At each time step, the program prints out the paths of the cars so far. After five time steps, the race is over.
This is the program:
In [34]:
from random import random
time = 5
car_positions = [1, 1, 1]
while time:
# decrease time
time -= 1
print('')
for i in range(len(car_positions)):
# move car
if random() > 0.3:
car_positions[i] += 1
# draw car
print('-' * car_positions[i])
The code is written imperatively. A functional version would be declarative. It would describe what to do, rather than how to do it.
A program can be made more declarative by bundling pieces of the code into functions.
In [39]:
from random import random
def move_cars():
for i, _ in enumerate(car_positions):
if random() > 0.3:
car_positions[i] += 1
def draw_car(car_position):
print('-' * car_position)
def run_step_of_race():
global time
time -= 1
move_cars()
def draw():
print('')
for car_position in car_positions:
draw_car(car_position)
time = 5
car_positions = [1, 1, 1]
while time:
run_step_of_race()
draw()