Home/Blog/Programming/Transducers I: Introduction to map, filter, and reduce
Home/Blog/Programming/Transducers I: Introduction to map, filter, and reduce

Transducers I: Introduction to map, filter, and reduce

8 min read
Dec 18, 2023
content
The map and filter functions
The reduce function
All you need is reduce
Chaining map and filter
Defining a compose function
Chaining map and filter using compose

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

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.

The map and filter functions#

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

The map function
The map function
The filter function
The filter function

This is how these functions can be used in Python:

numbers = [1, 3, 4, 7, 8, 11, 12, 15]
# compute squares of all numbers
squares = map(lambda x: x**2, numbers)
# filter out even numbers
evens = 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 is lambda <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 numbers
squares = my_map(lambda x: x**2, numbers)
# filter out even numbers
evens = my_filter(lambda x: x % 2 == 0, numbers)
print(list(squares))
print(list(evens))

The reduce function#

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:

  1. A reducer function is any function that takes two values and combines them into one (e.g., addition, multiplication, etc.)

  2. An input list

  3. An optional initializer object (if given, this is used in the first iteration as an initial value for the computation)

Schematic representation of a reducer
Schematic representation of a reducer

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.

Schematic representation of the reduce function
Schematic representation of the reduce function

Here’s how we can use the reduce function:

from functools import reduce
numbers = [1, 3, 4, 7, 8, 11, 12, 15]
# compute the sum of all numbers and add 10
sum_plus_10 = reduce(lambda x, y: x + y, # reducer function
numbers, # input list
10) # initial value
print(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:

  1. We start with 10 as the initial value

  2. Then, we get 10 + 1 = 11

  3. 11 + 3 = 14

  4. 14 + 4 = 18

  5. 18 + 7 = 25

  6. 25 + 8 = 33

  7. 33 + 12 = 55

  8. 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 acc
numbers = [1, 3, 4, 7, 8, 11, 12, 15]
# compute the sum of all numbers and add 10
sum_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.

All you need is reduce#

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 function
def 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 numbers
squares = 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**2] = [1]

  3. [1] + [3**2] = [1, 9]

  4. [1, 9] + [4**2] = [1, 9, 16]

  5. [1, 9, 16] + [7**2] = [1, 9, 16, 49]

  6. And so on

Similarly, we can define the filter function using reduce:

from functools import reduce
# Define the filter function using the reduce function
def 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 numbers
evens = 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:

  1. []

  2. [] + [] = [], since 1 is not even

  3. [] + [] = [], since 3 is not even

  4. [] + [] = [4], since 4 is even

  5. [4] + [] = [4], since 7 is not even

  6. [4] + [8] = [4, 8], since 8 is even

  7. [4, 8] + [11] = [4, 8], since 11 is not even

  8. [4, 8] + [12] = [4, 8, 12], since 12 is even

  9. [4, 8, 12] + [] = [4, 8, 12], since 15 is not even

Chaining map and filter#

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 numbers
evens = filter(lambda x: x % 2 == 0, numbers)
# compute squares of those numbers
result = 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.

Defining a compose function#

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 reduce
def 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 xxx \mapsto x), the reducer function takes the accumulated value val computed so far and the next function func from the list, and updates the accumulator with func(val). So, we get:

  1. xx

  2. f1(x)f_1(x)

  3. f2(f1(x))f_2(f_1(x))

  4. f3(f2(f1(x)))f_3(f_2(f_1(x)))

  5. \cdots

  6. fn1((f3(f2(f1(x)))))f_{n-1}(\cdots(f_3(f_2(f_1(x)))))

  7. fn(fn1((f3(f2(f1(x))))))f_n(f_{n-1}(\cdots(f_3(f_2(f_1(x))))))

Chaining map and filter using compose#

Let’s see how we can use this function to compose the two functions we had in our chain:

from functools import reduce
def 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 functions
composed = compose(lambda x: x % 2 == 0, lambda x: x**2)
# compute the final result in one go
result_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 reduce
def 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 squares
composed = compose(lambda x: x + 1, lambda x: x**2)
# compute the final result in one go
result = 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

Cover
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.

5hrs
Beginner
234 Playgrounds
12 Quizzes

Learn Functional Programming with Elixir

Cover
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.

11hrs 10mins
Beginner
123 Playgrounds
7 Quizzes

Learn Functional Programming in Haskell

Cover
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.

4hrs
Beginner
20 Challenges
10 Quizzes

Written By:
Imran Farid Khan
Join 2.5 million developers at
Explore the catalog

Free Resources