Transducers II: Filter and map transducers
In the Transducers I: Introduction to map, filter, and reduce blog, we introduced the map, filter, and reduce functions, and observed why there’s no simple way of chaining filter and map operations without producing intermediate lists. We saw this example:
We saw that the reason why this doesn’t work is that the functions provided to the map and the filter functions are of different types: the former is a transformation function whereas the latter is a criterion function.
What are transducers?#
This is where transducers come to the rescue. Let’s start with the definition:
Transducers are functions that take a reducer function as input and return another reducer function.
Recall that a reducer is any function that takes two parameters and reduces them to a single value.
Having a reducer as both the input and output makes transducers composable. This is how we'll use them.
But first we need to figure out how we can translate the filter and map functions we saw earlier into transducers.
The map transducer#
We’ll start with the map transducer. Recall that this is how we implemented the map function using the reduce function:
The mapping function#
Let’s define a mapping function, that takes a transformation function func as input (like the ones used to call the map function) and returns a map transducer, as follows:
There’s a lot going on here. Let’s unravel it.
First of all, verify that the function being returned by the mapping function on lines 6–8 is actually a transducer function. Note the signature, it takes a reducer named input_reducer as input, and returns another reducer as output:
(lambda a, x:input_reducer(a, func(x))
As this function takes two inputs and produces one output, it’s a reducer.
Note: We’ll see later what the significance of
input_reducer(a, func(x))is.
The function returned by mapping takes a reducer as an input and returns a reducer, so it is a transducer by definition.
But how do we use it?
Given a transformation function, we call the mapping function on it, and we get a transducer (map_transducer) as the result. We can then use the map_transducer to obtain the output_reducer function that we’ll use with the reduce function to implement the map function, as shown earlier.
But in order to make use of map_transducer, we need to supply an input reducer function input_reducer to it. What should this function be?
Recall that in our implementation of the my_map function using reduce earlier, we were appending the newly computed results to the accumulator to produce the final output (using the + operator):
lambda x, y: x + [func(y)]
We can observe that this appending step is actually a reducing operation, where we are taking two values—the accumulator a and the newly computed value func(x)—and producing the output list by the append operation. So the append function defined in lines 17–19 can be passed as the input input_reducer to the map_transducer function, and we do so on line 23 of the code to obtain the output_reducer function.
The effect of this reducer is that we would get append(a, func(x)) on line 8, which is equivalent to the reducer we passed in the implementation of the my_map function earlier.
One benefit of this abstraction#
This abstraction is born out of necessity, as we needed to provide some reducer as input to our map transducer, but it offers us an additional flexibility in manipulating the final result. For instance, we can add up the computed values func(x)—instead of appending them—with just one small change. We can provide an add function instead of the append function as our input reducer and adjust the initial value, from [] to 0 on line 26.
How nifty is that!
The filter transducer#
We can define the filter transducer more or less similarly, as shown below:
Note that the only essential difference is in line 8, where we apply the input_reducer to reduce conditionally.
Defining a transduce function#
Let’s now define a transduce function that makes it easy to deal with transducers. It would work like the reduce function.
It starts by using the provided reducer function with the transducer function to obtain a reducer function that we can use with the reduce function.
Composing transducers#
We now get to our main objective, that is, chaining transducers. Let's see how our simple example can be solved using transducers. In the following code widget, we’re defining the compose function in the transducer.py file, where we’ve moved some of the core functions.
Note that when defining the compose function in the transducer.py file, we are reversing the functions input parameters list on line 6, something that we didn’t do in our original implementation of the compose function. Can you explain why that is?
Composing more than two transducers#
We can, of course, use this machinery to compose any number of transducers:
Note: When executing this code, no intermediate lists are generated and each number is processed only once! This makes transducers of particular importance in processing streaming data.
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.