#each_slice and #flat_map: A Perfect Pair
The Enumerable#each_slice method is a great way to break up a collection so you can work on chunks of it at a time.
irb> (1..10).each_slice(2) { |slice| pp slice }
[1, 2]
[3, 4]
[5, 6]
[7, 8]
[9, 10]
=> nil
I find it useful when I need to query a bunch of records from a database with a
SQL IN
clause. Let’s say you have a table of items, and you want to query
order lines for the active items. Like this example:
item_numbers = Item.where(active: true).map(&:item_number)
lines = OrderLine.where(item_number: item_numbers)
# SELECT * FROM order_lines WHERE item_numbers IN ( ... )
Some databases
put a limit on how many values you can use on an IN
clause. So if your active
items exceed that limit, the query will fail. That’s where #each_slice
can
help.
lines = []
item_numbers.each_slice(100) do |slice|
lines += OrderLine.where(item_number: slice)
end
Now we’re performing multiple queries with only 100 item numbers at a time. Cool! That fixes our query problem, but this code is kinda ugly. I don’t like creating empty arrays just to build them with a loop unless there’s no other way to avoid it. That’s where Enumerable#flat_map comes to the rescue.
lines = item_numbers.each_slice(100).flat_map do |slice|
OrderLine.where(item_number: slice)
end
#flat_map
is the perfect companion to #each_slice
. When #each_slice
tears
the collection apart, #flat_map
is there to put all of the pieces back
together.
I particularly enjoy how #each_slice
returns an Enumerable
if a block isn’t
given, which means you can immediately call #flat_map
on the return value. The
slices are passed to the #flat_map
method and then flattened back into a
single collection when it’s done. Brilliant!