Transduce over any Iteratable Collection

Paul Frend
InstructorPaul Frend
Share this video with your friends

Social Share Links

Send Tweet
Published 7 years ago
Updated 5 years ago

So far we've been transducing by manually calling .reduce() on arrays, but we want to be able to transduce over other collection types as well.

In this lesson we'll create a transduce function to support transducing over any data structure that implements the es2015 iterable protocol. We’ll put it to the test by transducing over strings and maps, as well as going from one collection type as input to another as output.

Instructor: [00:00] Let's begin by parameterizing our previous transduction. Everything that's going on up here, we want to put into a new function. Let's call this transduce, and we want this to do exactly what we're doing above.

[00:13] It will take some arguments, and then perform the same thing. The first argument will be our transform. If this is our transducers, which could be composed, then we'll take our inner reducer, which is this guy, and then we'll take our initial seed, and finally, our collection.

[00:32] This is our initial seed, and this is our collection. Then we'll just put these pieces together again. We will reduce over our collection, calling our transform with our reducer and our seed. Let's try this out. First, we'll just paste in the transducers we created from before.

[00:51] Then I will call transduce, and I'll put these on separate lines. Let's create our transform. We'll use is not two filter, is even filter, and double map. We'll use our push reducer as the inner reducer, an empty array as our seed value, and one, two, three, four as our collection.

[01:14] Let's verify the result. We see we're left with the value eight, which is the same as we got before. Now, we've black boxed our iteration, meaning the fact that this transduce function iterates through the collection with a call to reduce is hidden from the consumer of this function.

[01:32] That's the first step to making this transduce function reusable across different collection types. At the moment, it will only work with a collection that has a reduce method. That's the interface that we've coupled ourselves to. The rest, we're in control of through all the arguments.

[01:47] Now, let's make it more generic, so that we can transduce over any iterable. To manage this, we're going to recreate reduce, but as a for-of loop. Let's comment out this line, and then we're just going to loop through our collection.

[02:00] We know we want to build up an accumulation, so let's start by capturing that in a variable. We also know we want this accumulation to start off as the value of our seed. Then let's create our loop. In here, we just want to build up our accumulation.

[02:18] The value of this accumulation is going to be the result of calling a transform with our reducer, which creates our transformed reducer. Then we will call that with our accumulation and our value. However, creating this transformed reducer has the same output every time.

[02:38] Let's extract that to a variable. We'll call that TransformedReducer, and let's define that up here. As the final step, we just want to return our accumulation. Now, since we're using this for-of loop, we're no longer coupled to working with arrays.

[02:56] Instead, we can use it with anything that implements the iterable protocol. The iterable protocol was added with ES2015, and the types that support it are maps, sets, arrays, and strings. Let's put it to test with a string.

[03:11] First, let's verify that it still works with our array, let's look at the same result as before. Then let's do a transduction that changes a lowercase string to the uppercase version. We need to our toUpper transformer, and then we can call transduce.

[03:27] We want our map transducer with the toUpper transformer. As the inner reducer, we just want a simple string addition. The accumulation here would be the string. The value will be a character. The new accumulation will be the string, plus the character.

[03:44] The seed value will be an empty string. Let's use the name as Adrian as our collection. Let's see how that works. As expected, we get our uppercase version of Adrian back. That works, but let's make it a bit more complex, and add filtering logic to only include vowels.

[04:01] Let's create an isVowel predicate. That will take a character, and I'm just going to paste in the vowels here. It will return true if our character is included in those vowels. Let's add that into our transduce call. Now, we can see that reflected in the result.

[04:24] As another example, let's keep working with the same composition we did down here, but with a map as the collection type instead of an array. Let's create our new map up here. I'm just going to paste in some values.

[04:38] Then instead of our array, let's use our map. We can't, however, use the map as it is. We have to specify if we want to iterate through the values or the keys. Let's use the values. Let's see if this works. Just like before, we get our result back of eight.

[04:54] This is pretty cool, because what's important to understand here is that the call to values does not create an array. It creates an iterable. This iterable doesn't have a reduce function. We can prove that by trying to call it.

[05:07] If we call num.Map.values, and then we try and call reduce, you can see we get an error saying that reduce is not a function. The reason we can transduce over it is purely because we rewrote our transducer helpers to iterate with the for-of loop, instead of calling reduce.

[05:24] Another thing that's pretty cool is that we're going from one collection type in our map into another collection type as our output, which is our array.

Anthony Ershov
Anthony Ershov
~ 6 years ago

Hi, Paul! The transduce function works perfectly with arrays but there are some issues with strings: http://take.ms/e7fve

I guess your imported functions from './utils' differ from functions in the previous video. My code is here https://stackblitz.com/edit/react-b77tn4

Bijoy Thomas
Bijoy Thomas
~ 6 years ago

Hello Anton, your map and filter functions are missing the return statements. They should be like:

const map = (xf) => reducer => {
    return (acc, val) => {
      // for string concat, the original string is NOT modified,
      // unlike the push reducer which mutates the array
        return reducer(acc, xf(val)); 
    };
};

const filter = (predicate) => reducer => {
    return (acc, val) => {
        if (predicate(val)) return reducer(acc, val); // same reason here
        return acc;
    };
};
Anthony Ershov
Anthony Ershov
~ 6 years ago

Bijoy, thanks a lot! Really helpful!

Bijoy Thomas
Bijoy Thomas
~ 6 years ago

Glad it helped!

Paul Frend
Paul Frendinstructor
~ 6 years ago

Thanks for responding Bijoy! Yep that's spot on, you should not be relying on the fact that the push reducer mutated the array. I never added the return statement into the map function in lesson 4, but it should be there (and is there in the version imported from utils.js). I thought I flagged this error here already but it must have been in a twitter response, so sorry for the confusion.

sebastian
sebastian
~ 6 years ago

hi @paul, what editor/plugin do you use to see the answers to your functions using / *? * /

Paul Frend
Paul Frendinstructor
~ 6 years ago

hi @paul, what editor/plugin do you use to see the answers to your functions using / *? * / hey @sebastian, it's called quokka.js

Markdown supported.
Become a member to join the discussionEnroll Today