Suppose you’re given a list, and you want to do some computations on it. Mostly, if not always, your computations will be one of the following three types:
transforming each element of the list in some way
filtering out elements of the list based on some criterion
computing some aggregate function on the list to reduce it to a single value
These situations are so pervasive that most languages nowadays provide specific functions for these operations, namely, the map
, filter
, and reduce
functions. Let’s examine these functions more closely.
Note: Though the following discussion is easily generalizable to all iterables, we’ll refer to all collections as lists for ease of exposition.
Most of us are somewhat familiar with the map
and filter
functions, as they have become more or less standard list operations. In summary:
The map
function applies a function on each element of the list
The filter
function filters the input list according to some criterion
This is how these functions can be used in Python:
numbers = [1, 3, 4, 7, 8, 11, 12, 15]# compute squares of all numberssquares = map(lambda x: x**2, numbers)# filter out even numbersevens = filter(lambda x: x % 2 == 0, numbers)print(list(squares))print(list(evens))
Note: The
lambda
keyword is used to define anonymous functions comprising a single expression in Python. The syntax islambda <arguments>: <expression>
.
It’s actually straightforward to implement these functions:
def my_map(func, l):return [func(x) for x in l]def my_filter(func, l):return [x for x in l if func(x)]numbers = [1, 3, 4, 7, 8, 11, 12, 15]# compute squares of all numberssquares = my_map(lambda x: x**2, numbers)# filter out even numbersevens = my_filter(lambda x: x % 2 == 0, numbers)print(list(squares))print(list(evens))
Another function that’s used to compute some aggregate value over some list is the reduce
function. Let’s take a look at this function from the functools
library. It takes in three parameters:
A reducer function is any function that takes two values and combines them into one (e.g., addition, multiplication, etc.)
An input list
An optional initializer object (if given, this is used in the first iteration as an initial value for the computation)
Here’s how the reduce
function works. It starts by initializing an accumulator object with the provided initial value, or with the first element of the list if an initial value is not provided. It then iterates through the rest of the list one object at a time, using the reducer function in each iteration with the current value of the accumulator as the first parameter and the object being processed in the current iteration as the second parameter to compute the updated accumulator.
Here’s how we can use the reduce
function:
from functools import reducenumbers = [1, 3, 4, 7, 8, 11, 12, 15]# compute the sum of all numbers and add 10sum_plus_10 = reduce(lambda x, y: x + y, # reducer functionnumbers, # input list10) # initial valueprint(sum_plus_10)
Let’s go through this execution. The reduce
function initializes an accumulator with the provided initial value, 10
. Then, it applies the provided reducer function lambda x, y: x + y
, passing the accumulator as x
and the current element of the list as y
, updating the accumulator in each iteration, as shown below:
We start with 10
as the initial value
Then, we get 10 + 1 = 11
11 + 3 = 14
14 + 4 = 18
18 + 7 = 25
25 + 8 = 33
33 + 12 = 55
Finally, we get 55 + 15 = 70
It’s fairly simple to write our own implementation of the reduce
function:
def my_reduce(reducer, l, initial=None):iterator = iter(l)acc = initial if initial is not None else iterator.next()for x in iterator:acc = reducer(acc, x)return accnumbers = [1, 3, 4, 7, 8, 11, 12, 15]# compute the sum of all numbers and add 10sum_plus_10 = my_reduce(lambda x, y: x + y,numbers,10)print(sum_plus_10)
In line 3, we initialize the accumulator acc
with the initial value if it is provided, otherwise we initialize it with the first element of the list.
In lines 4–5, we loop over the list, apply the reducer
function on acc
and the current element x
of the list, and update acc
with the returned value.
Finally, in line 6 we return acc
, which contains the desired result.
If you think a bit more abstractly, you'll notice that the lists returned by the map
and filter
functions can be thought of as the result of a computation over the input list. That is, we've reduced the input list to the output list. This would imply that the reduce
function is actually more basic, and both the map
and the filter
functions can be thought of as its applications.
It’s fairly straightforward to implement these functions by using just the reduce
function. Let’s take a look at the map
function first:
from functools import reduce# define the map function using the reduce functiondef my_map(func, l):return reduce(lambda x, y: x + [func(y)],l,[])numbers = [1, 3, 4, 7, 8, 11, 12, 15]# compute squares of all numberssquares = my_map(lambda x: x**2, numbers)print(list(squares))
Using the empty list []
as the initial value of the accumulator, the reducer function reduces the accumulator list computed so far and concatenates it (using the +
operator) with a list containing just one element func(y)
(i.e., the current element y
of the list with the provided function func
applied on it). The final output list is built incrementally:
[]
[] + [1**2] = [1]
[1] + [3**2] = [1, 9]
[1, 9] + [4**2] = [1, 9, 16]
[1, 9, 16] + [7**2] = [1, 9, 16, 49]
And so on
Similarly, we can define the filter
function using reduce
:
from functools import reduce# Define the filter function using the reduce functiondef my_filter(func, l):return reduce(lambda x, y: x + [y] if func(y) else x,l,[])numbers = [1, 3, 4, 7, 8, 11, 12, 15]# filter out even numbersevens = my_filter(lambda x: x % 2 == 0, numbers)print(list(evens))
As before, taking the empty list []
as the initial value of the accumulator, the reducer function builds the final list incrementally, but this time using the provided function as the condition based on which either the list containing the current element [y]
or the empty list []
is appended to the accumulator. This is how the computation goes:
[]
[] + [] = []
, since 1
is not even
[] + [] = []
, since 3
is not even
[] + [] = [4]
, since 4
is even
[4] + [] = [4]
, since 7
is not even
[4] + [8] = [4, 8]
, since 8
is even
[4, 8] + [11] = [4, 8]
, since 11
is not even
[4, 8] + [12] = [4, 8, 12]
, since 12
is even
[4, 8, 12] + [] = [4, 8, 12]
, since 15
is not even
In many applications—for instance, in data preprocessing—we need to apply various transformations and filters on a list sequentially. As a simple example of this, consider the case where we want to first filter out even numbers and then compute squares of those numbers.
numbers = [1, 3, 4, 7, 8, 11, 12, 15]# filter out even numbersevens = filter(lambda x: x % 2 == 0, numbers)# compute squares of those numbersresult = map(lambda x: x**2, evens)print(list(result))
One disadvantage of chaining these operations in this way is that it produces intermediate lists. In the example given above, we don’t really need the evens
list, but we need to generate it to pass it to the map
function. Even if we move the call to the filter
function to the call in the map
function, a temporary list will still be produced. This becomes problematic, especially when we’re dealing with large lists and when there are several operations we need to chain.
To chain map
and filter
functions without producing intermediate lists, why can’t we simply compose the functions provided to map
and filter
?
The reason is that the functions provided to the map
and filter
functions are of different types. The functions provided to the map
function are transformations, that is, they transform an element to some other element. Whereas the functions provided to the filter
function are the criteria based on which an element is selected to be in the final output. The output of the functions provided to the filter
function is always boolean.
Let’s look at this problem a bit more closely.
Let’s look at how we can compose multiple functions into one. That is, given a bunch of functions,
we want to be able to compose them
so that
We can do so in a straightforward manner using the reduce
function we’ve seen earlier:
from functools import reducedef compose(*functions):return lambda x: reduce(lambda val, func: func(val),functions,x)
The compose
function returns the composed function that takes the parameter x
and applies each function in the functions
list. That is, with x
as the initial value of the accumulator (i.e., x
serving as the output of the identity function val
computed so far and the next function func
from the list, and updates the accumulator with func(val)
. So, we get:
Let’s see how we can use this function to compose the two functions we had in our chain:
from functools import reducedef compose(*functions):return lambda x: reduce(lambda val, func: func(val),functions,x)numbers = [1, 3, 4, 7, 8, 11, 12, 15]# compose the functions provided to the filter# and map functionscomposed = compose(lambda x: x % 2 == 0, lambda x: x**2)# compute the final result in one goresult_map = map(composed, numbers)result_filter = filter(composed, numbers)print(list(result_map))print(list(result_filter))
Note that the first function—the filter
function lambda x: x % 2 == 0
—maps the input value it receives to a boolean value, therefore the map
function invoked after it gets only the boolean values to process. This is not what we want.
This will work if we have just the map
functions in our chain.
from functools import reducedef compose(*functions):return lambda x: reduce(lambda val, func: func(val),functions,x)numbers = [1, 3, 4, 7, 8, 11, 12, 15]# compose the function that increments# with the function that squarescomposed = compose(lambda x: x + 1, lambda x: x**2)# compute the final result in one goresult = map(composed, numbers)print(list(result))
But what we want is the flexibility of chaining any number of operations (of any type) in any order. This problem can be solved by transducers, which will be a topic for our next blog.
Do note that we can't compose the reducer functions used to implement the map
and filter
functions either, for the simple reason that a reducer requires two inputs and produces one output. Transducers will solve this problem as well. Understanding the limitations of traditional reducer functions in composing operations, we'll explore how transducers provide an innovative approach to effectively combine map, filter, and reduce functions for more efficient data processing.
If you want to learn more about how we can simplify programming using functions, these ideas are explored in a more systematic way in the functional programming paradigm. Consider taking the following courses on Educative:
Learn Functional Programming in Python
The functional programming paradigm can be a powerful tool, especially as it can be integrated seamlessly with procedural and object-oriented code in Python. In this course, you’ll learn what functional programming is, how it’s used, and the features of Python that support it. To start, you’ll learn how functions act as objects, the role of mutability, and how to perform recursion. In the latter half of the course, you’ll focus on closures, iterables & iterators, generators, and more. Throughout the course will be three exams which will test your understanding and really drive home what you’ve learned. By the end, you’ll have a new programming paradigm to add under your belt and have the confidence to start using functional programming in your own projects.
Learn Functional Programming with Elixir
Elixir is a functional programming language that runs in the Erlang VM, a powerful environment to run distributed systems. This course uses Elixir because of its fun syntax, vibrant community, and production-ready tooling. Elixir syntax lets you focus on what’s important while learning functional programming. Starting from scratch, you will learn the fundamentals of Elixir including expressions, modules, conditional statements, and recursion. Towards the end of the course, you will focus on more advanced material like higher-order functions, how to handle impure functions, and lastly how to design an application using Elixir and functional programming principles. By the end, you will be able to write basic Elixir programs that follow the functional programming paradigm.
Learn Functional Programming in Haskell
Functional programming is a problem-solving paradigm that differs drastically from the imperative programming style of languages like C++ or Java. While functional programming principles have invaded most modern programming languages to some degree, there is no language that embraces this paradigm quite as Haskell does. As a result, Haskell code can reach a level of expressiveness, safety, and elegance that is difficult to achieve in other programming languages. This course will introduce you to the core concepts of functional programming in Haskell. You will learn how to write pure functions using techniques such as pattern matching and recursion, and how to work with Haskell's powerful List data type. You will understand the basics of Haskell's powerful type system and define your own data types. Finally, you’ll learn how to perform IO operations the Haskell way. By the end of this course, you’ll have a new paradigm to add under your belt and you can start using functional programming in your own projects.
Free Resources