The main differences between standard (eager) enumerators and lazy ones lie in how they process data through chains of enumerations. Standard enumerators handle this in a sequential manner, while lazy enumerators pass each value through the entire chain one by one.
def do_a(x)
puts "Doing A with #{x}"
x
end
def do_b(x)
puts "Doing B with #{x}"
x
end
result = (1..5)
.map { do_a(it) }
.map { do_b(it) }
.take_while { it < 3 }
.to_a
# Doing A with 1
# Doing A with 2
# Doing A with 3
# Doing A with 4
# Doing A with 5
# Doing B with 1
# Doing B with 2
# Doing B with 3
# Doing B with 4
# Doing B with 5
puts result.inspect # [1, 2]
# ----
result = (1..5)
.lazy
.map { do_a(it) }
.map { do_b(it) }
.take_while { it < 3 }
.to_a
# Doing A with 1
# Doing B with 1
# Doing A with 2
# Doing B with 2
# Doing A with 3
# Doing B with 3
puts result.inspect # [1, 2]
There are three main benefits of using lazy enumerators:
- You can stop early. No need to process all the data if you've already gathered the required results. Methods like
firstandtake_whileare your friends here. - They are CPU-efficient. By using enumerators like
select, you can stop further processing of an item at any step of the chain. - They are memory-efficient. They don't build intermediate arrays, so the data used in processing can be released right after handling the current item. This is especially useful when dealing with files or fetching data from URLs.