A Common Example…
Consider the following snippet that transforms a collection; first by filtering to meet a some predicates and then by applying a transformation via map.
Immutable.List([1,2,3,4,5,6,7,8,9,10]).filter(value => value <5).filter(value => value %2===0).map(value => value *10);// List [20, 40]
The code above will generate a new list of values as soon as the expression is parsed. At each step the above will generate an intermediary collection that has the next action in the chain applied. If we unchain the transformation we may end up with something like below:
let fullList = Immutable.List([1,2,3,4,5,6,7,8,9,10]);let lessThan5List = fullList.filter(value => value <5);// List [1, 2, 3, 4]let evenLessThan5List = lessThan5List.filter(value => value %2===0);// List [2, 4]let multiples10LessThan5List = evenLessThan5List.map(value => value *10);// List [20, 40]
In this second example the intermediary collections are more apparent. Each subsequent collection is smaller, we are continually iterating over the same few elements. Looking at the above code, how many times does the value 2 gets inspected across all the lists? (hint: it’s more than once)
Considering the original chained version of the code there is one common argument against chaining multiple filters and maps. That argument is that, “we should reduce the number of times we iterate the collection”. And the solution comes down to implementing something as follows:
While this solution is not terrible, using a reducer has a certain amount of additional overhead. Notably the conditions for keeping values are the complement(opposite), and the transformation actions is surrounded by some noise from lower level operations(pushing to the accumulator).
I am not the biggest fan of the using the reducer to circumvent the multiple iterations. Having multiple filter and map actions describe the transformation of the collection with much less overhead to the reader. Thus we arrive at a sort of impasse where the base (List, Set, Map) collections are not enough to provide the preferred performance and expression.
Let’s be Lazy…
Seq is a lazy construct that allows us to separate the description of a computation and the execution of that computation. The code below is a description of the computation that needs to happen, not unlike the original snippet above. The collection is wrapped by Seq and it is not iterated until the it is read. The following chaining of filter and map actions each create a Seq, which can be seen as a stream of values; each value passing through each of the transformation actions at once.
let pendingSeq = Immutable.Seq([1,2,3,4,5,6,7,8,9,10]).filter(value => value <5).filter(value => value %2===0).map(value => value *10);pendingSeq.get(0) // Finally Computes first value.
Seq iterates the original collection once but has the same net effect as the actions taken in List, Map, and Set which iterate the entire collection, creating intermediary collections at each step, which then get iterated again at the next step.
Below are two diagrams to help illustrate the difference in how the values are computed to compute a final list.
Now we can rest assured that by using Seq we can have our expressivity and performance at the same time. However Seq do have a few gotchas that have the possibility of undoing our previous improvements.
While Seq is a lazy data structure it is not a memoized lazy data structure. This means that every time we read a value from our sequence such as mySeq.get(10) the iteration to get to the value at index 10 from our original collection has to start over. For this reason when you are ready to start using the results of Seq, it may be best to convert back to a non-lazy data structure like a List or Map.
Immutable.js allows us to do this by invoking helper methods toList(), toMap(), toSet() and numerous others. This forces evaluation of our actions.
// See above for definition of `pendingSeq`// Time to use our list:let myList = pendingSeq.toList();console.log("Top 2 Results Are:")console.log(pendingSeq.get(0))console.log(pendingSeq.get(1))
The snippet about would work just fine if we did not cast with toList however, every time we read a value the values leading up to the index would have to be computed also. When getting index 0 we just compute the first value. When getting index 1 Seq also has to compute index 0 again, as well as index 1, so on and so forth. This gives us an approximate complexity of O(n^2) if we iterate over the indices individually. To see this in action checkout this js bin: https://jsbin.com/qexularabe/edit?js,console.
Getting a Seq
If you are already using an the Seq data structure, Immutable provides a helper method: toSeq(). This helper yields a lazy Seq that you can use as illustrated in this post. If we return to our original snippet that showed the expressivity we desired we can see how simple it can be to use Seq to keep the expressiveness of multiple actions, and the performance of only iterating the original collection once.
Immutable.List([1,2,3,4,5,6,7,8,9,10]).toSeq().filter(value => value <5).filter(value => value %2===0).map(value => value *10).toList();// List [20, 40]
Hopefully I’ve been able to make the case for using Seq in Immutable.js and given you a glimpse into the possibility for lazy data structures and transducers. May your expressions remain readable and performant.