Lazy enumerators run the whole chain per element
Question
What is the return value of the following Ruby code?
a = []
upcase = ->(c) { a << c; c.upcase }
downcase = ->(c) { a << c; c.downcase }
%w(a b c).lazy.map(&upcase).map(&downcase).force
a.join # => ???
The correct answer is
-
"ABCabc" -
"aAbBcC"Correct -
"abcABC"
Explanation
TL;DR
With Enumerable#lazy, the chained map calls do not run as separate passes. Each element travels through the whole chain before the next element starts: "a" is upcased then downcased, then "b", then "c". Both lambdas log the value they receive, so a collects "aAbBcC".
Step by step
a = []
upcase = ->(c) { a << c; c.upcase }
downcase = ->(c) { a << c; c.downcase }
%w(a b c).lazy.map(&upcase).map(&downcase).force # => ["a", "b", "c"]
a.join # => "aAbBcC"
For each element, upcase logs the original character and returns its uppercase version, then downcase logs that uppercase character and returns it lowercased. Unrolled, the lazy chain is equivalent to:
a = []
chars = %w(a b c)
new_chars = []
upcase = ->(c) { a << c; c.upcase }
downcase = ->(c) { a << c; c.downcase }
new_chars[0] = downcase.(upcase.(chars[0])) # a << "a", then a << "A"
new_chars[1] = downcase.(upcase.(chars[1])) # a << "b", then a << "B"
new_chars[2] = downcase.(upcase.(chars[2])) # a << "c", then a << "C"
a.join # => "aAbBcC"
The eager version
Without lazy, each map is a full pass over the collection. The first map logs "a", "b", "c" and returns ["A", "B", "C"]; the second pass then logs "A", "B", "C":
a = []
upcase = ->(c) { a << c; c.upcase }
downcase = ->(c) { a << c; c.downcase }
%w(a b c).map(&upcase).map(&downcase) # => ["a", "b", "c"]
a.join # => "abcABC"
The final result of the chain is identical; only the order of evaluation differs. That per-element order is exactly what makes lazy useful: combined with first(n) or an infinite source, it only processes the elements it needs.
(1..Float::INFINITY).lazy.map { |x| x * 2 }.first(3) # => [2, 4, 6]
Share this quiz
Comments
No comments yet. Be the first.