Transducers are built upon the design princlple in Clojure of composing functions together, allowing you to elegantly abstract functional composition and create a workflow that will transform data without being tied to a specific context. So what does that actually mean and what does the code look like? Is there a transducer function or is it just extensions to existing functions. These are the questions we will explore and answer.
If you are in the early stages of learning Clojure, then I suggest getting your head around functions such as map & reduce and composing functions with the threading macros before diving into Transducers.
This is my interpretation of the really great introduction to Transducers from Clojurescript Unraveled, expanded with additional code and my own comments.
Defining a data structure that will represent our fruit, including whether that fruit is rotten or clean. We have two collections of grapes, one green, one black. Each cluster has 2 grapes on it (not a very big cluster in this example)
Each grape cluster has the following structure
We want to split the grape clusters into individual grapes, discarding the rotten grapes. The remaing grapes will be checked to see if they are clean. We should be left with one green and one black grape.
First lets define a function that returns a collection of grapes, given a specific grape cluster.
The body of this function returns the value pointed to by the
:grapes keyword, which will be a collection of grapes. We do not ask for the value of :colours as in this case the colour of the grape is irelevant.
The grape-clusters data structure is a vector of two grape clusters. To see what a grape cluster is, get the first element of that data structure
For each cluster in grape-clusters, return just the :grapes data, ignoring the colour information
We dont want to include any rotten grapes after we have processed all our clusters, so here we define a simple filter to only return grapes where
:rotten? is false.
This filter will be used on each individual grape extracted from the cluster.
Any grapes we have left should be cleaned. Rather than model the cleaning process, we have simply written a function that updates all the grapes with a value of
true for the key
Lets give our clean grape function a quick test in the REPL.
Each line passes its evaluate value to the next line as its last argument. Here is the algorithm we want to create with our code:
- evaluate the name grape-clusters and return the data structure it points to.
- use mapcat to map the split-clusters function over each element in grape-clusters, returning 4 grapes concatinated into one collection
- filter the 4 grapes, dropping the grapes where :rotten? equals true, returning 2 grapes
- update each grape to have a :clean? value of true
Composing functions are read in the lisp way, so we pass the grape-clusters collection to the last composed function first
Now lets call this composite function again…
process-clusters definition above uses the lisp way of evaluation - inside-out.
Here is a simple example of evaluating a maths expression from inside-out. Each line is the same expression, but with the innermost expression replaced by its value.
There are several functions that work on sequences (collections) which will return what is refered to as a transducer if they are not passed a sequence as an argument. For example, if you only pass map a function and not a collector, it returns a transducer that can be used with a collection that is passed to it later.
Using the transduce feature of each of the functions in process-clusters, we can actually remove the partial function from our code and redefine a simpler version of process-clusters
A few things changed since our previous definition process-clusters. First of all, we are using the transducer-returning versions of mapcat, filter and map instead of partially applying them for working on sequences.
Also you may have noticed that the order in which they are composed is reversed, they appear in the order they are executed. Note that all map, filter and mapcat return a transducer. filter transforms the reducing function returned by map, applying the filtering before proceeding; mapcat transforms the reducing function returned by filter, applying the mapping and catenation before proceeding.
One of the powerful properties of transducers is that they are combined using regular function composition. What’s even more elegant is that the composition of various transducers is itself a transducer! This means that our process-cluster is a transducer too, so we have defined a composable and context-independent algorithmic transformation.
Many of the core ClojureScript functions accept a transducer, let’s look at some examples with our newly defined version of
Since using reduce with the reducing function returned from a transducer is so common, there is a function for reducing with a transformation called transduce. We can now rewrite the previous call to reduce using transduce:
This was just a brief taste of Transducers in Clojure and I hope to create more examples of their use over time. I dont see Transducers being used too much for my own code initially, but its a useful way to abstract functional composition and make your code more reusable within your project.
If you need more time for this concept to sink in, its quite alright to stay with threading macros and the partial function, or even just applying map. I find Clojure more rewarding when you first get more comfortable with the core concepts and build on them when you are ready.
This work is licensed under a Creative Commons Attribution 4.0 ShareAlike License, including custom images & stylesheets. Permissions beyond the scope of this license may be available at @jr0cket