Comparison to iterators
How foldl is used illustrates the difference between iterators and transducers. Consider a transducer
julia> using Transducers
julia> xf = opcompose(Filter(iseven), Map(x -> 2x));which works as
julia> collect(xf, 1:6)
3-element Vector{Int64}:
4
8
12
julia> foldl(+, xf, 1:6) # 4 + 8 + 12
24Implementation of the above computation in iterator would be:
f(x) = 2x
imap = Base.Iterators.Generator # like `map`, but returns an iterator
mapfoldl(f, +, filter(iseven, input), init=0)
foldl(+, imap(f, filter(iseven, input))) # equivalent
# ______________________________
# composition occurs at input partCompare it to how transducers are used:
foldl(+, opcompose(Filter(iseven), Map(f)), input, init=0)
# _________________________________
# composition occurs at computation partAlthough this is just a syntactic difference, it is reflected in the actual code generated by those two frameworks. The code for iterator would be lowered to:
function map_filter_iterators(xs, init)
ret = iterate(xs)
ret === nothing && return init
acc = init
@goto filter
local state, x
while true
while true # input
ret = iterate(xs, state) #
ret === nothing && return acc #
@label filter #
x, state = ret #
iseven(x) && break # filter :
end # :
y = 2x # imap : :
acc += y # + : : :
end # : : : :
# + <-- imap <-------- filter <-- input
endNotice that the iteration of input is the inner most block, followed by filter, imap, and then finally +. Iterators are described as pull-based; an outer iterator (say imap) has to "pull" an item from the inner iterator (filter in above example). It is reflected in the lowered code above.
On the other hand, the code using transducers is lowered to:
function map_filter_transducers(xs, init)
acc = init
# input -> Filter --> Map --> +
for x in xs # input : : :
if iseven(x) # Filter : :
y = 2x # Map :
acc += y # +
end
end
return acc
end
xs = [6, 8, 1, 4, 5, 6, 6, 7, 9, 9, 7, 8, 6, 8, 2, 5, 2, 4, 3, 7]
@assert map_filter_iterators(xs, 0) == map_filter_transducers(xs, 0)Notice that the iteration of input is at the outer most block while + is in the inner most block. Transducers passed to foldl appears in the block between them in the order they are composed. An outer transducer (say Filter) "pushes" arbitrary number of items to the inner transducer (Map in above example). Note that Filter can choose to not push an item (i.e., push zero item) when the predicate returns false. This push-based nature of the transducers allows the generation of very natural and efficient code. To put it another way, the transducers and transducible processes own the loop.
As a consequence, computations requiring to expand an item into a sequence can be processed efficiently. Consider the following example:
julia> xf = opcompose(Map(x -> 1:x), Filter(iseven ∘ sum), Cat())
foldl(*, xf, 1:10)
29262643200This is lowered to a nested for loops:
function map_filter_cat_transducers(xs, init)
acc = init
for x in xs
y1 = 1:x # Map
if iseven(sum(y1)) # Filter
for y2 in y1 # Cat
acc *= y2 # *
end
end
end
return acc
end
@assert foldl(*, xf, 1:10) == map_filter_cat_transducers(1:10, 1)It is not straightforward to implement an iterator like Cat that can output more than one items at a time. Such an iterator has to track the state of the inner (y1 in above) and outer (xs in above) iterators and conditionally invoke the outer iterator once the inner iterator terminates. This generates a complicated code and the compiler would have hard time optimizing it.