Enumerable#each_with_object
The each_with_object method iterates over an enumerable, passing each element and a mutable object you provide. Unlike inject/reduce, the accumulator object is returned unchanged at the end, so you control how the object grows inside the block.
That makes each_with_object a good fit when you want to build a hash, array, or custom accumulator without reassigning it on every iteration. The block stays focused on mutation, which often makes the intent easier to read than a more general reduction.
how it works
Unlike inject/reduce where the return value of the block becomes the accumulator, each_with_object keeps your object intact and lets you mutate it directly:
result = collection.each_with_object(initial_object) { |element, accumulator|
# modify accumulator in place
}
The object you pass is the one that gets returned at the end.
That behavior is helpful when the accumulator has identity, such as a hash with default values or an array that you want to append to in place. It also means the block return value can stay incidental, because the object itself carries the result forward.
basic usage
Building an array of doubled values:
numbers = [1, 2, 3, 4, 5]
doubled = numbers.each_with_object([]) { |n, arr| arr << n * 2 }
# => [2, 4, 6, 8, 10]
Starting with an empty hash as the accumulator lets you build a lookup table directly from the iteration. Each key-value pair is inserted inside the block, so the output shape matches whatever structure you need for the next processing step.
Building a hash from an array:
words = ["apple", "banana", "cherry"]
lengths = words.each_with_object({}) { |word, hash| hash[word] = word.length }
# => {"apple"=>5, "banana"=>6, "cherry"=>6}
This pattern appears often in data shaping code because it keeps the transformation and the destination together. Instead of building an array and converting it later, you can write directly to the structure you want. The difference between each_with_object and inject matters when the accumulator should stay the same object across every iteration.
why use each_with_object over inject?
The key difference is mutability. With inject/reduce, the accumulator is reassigned each iteration:
# inject - accumulator is reassigned each time
[1, 2, 3].inject(0) { |acc, n| acc + n }
# => 6 (acc gets new value each iteration)
The inject form reassigns the accumulator on every iteration, which works well for arithmetic but requires an explicit return of the modified object. Each_with_object avoids that requirement because the same object persists across iterations.
With each_with_object, you start with a specific object and mutate it:
# each_with_object - you control the exact object returned
[1, 2, 3].each_with_object([]) { |n, arr| arr << n }
# => [1, 2, 3] - the exact array you passed in, now populated
This distinction matters when the accumulator already exists and you want to preserve it as-is. A mutable hash or array is often clearer than returning a brand new object from every iteration, especially when the transformation is mostly about collecting values. The practical examples below show how this plays out with grouping, nesting, and multi-bucket collection.
practical examples
grouping elements by a criterion
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
grouped = numbers.each_with_object({ even: [], odd: [] }) do |n, hash|
n.even? ? hash[:even] << n : hash[:odd] << n
end
# => {even=>[2, 4, 6, 8, 10], odd=>[1, 3, 5, 7, 9]}
Grouping by a condition is one of the most natural uses for each_with_object. The block can route each value into the right bucket while keeping the grouping structure visible and easy to extend. Nested structures work the same way, where the accumulator starts as a hash of arrays and each record lands under the correct key.
building a nested hash
orders = [
{ customer: "Alice", item: "Book", price: 15 },
{ customer: "Bob", item: "Pen", price: 3 },
{ customer: "Alice", item: "Pen", price: 2 }
]
by_customer = orders.each_with_object(Hash.new { |h, k| h[k] = [] }) do |order, hash|
hash[order[:customer]] << { item: order[:item], price: order[:price] }
end
# => {"Alice"=>[{:item=>"Book", :price=>15}, {:item=>"Pen", :price=>2}],
# "Bob"=>[{:item=>"Pen", :price=>3}]}
The nested hash example shows another common pattern: use the accumulator to store intermediate structure, not just a final summary. This keeps related records together without creating a second pass over the data.
That approach keeps the grouping code compact and the result shape predictable. Collecting into multiple buckets at once shows another strength: a single pass can classify each element into several categories without splitting the source data repeatedly.
collecting multiple results at once
data = [1, 2, 3, 4, 5]
results = data.each_with_object({ positives: [], negatives: [], zeros: [] }) do |n, hash|
if n > 0
hash[:positives] << n
elsif n < 0
hash[:negatives] << n
else
hash[:zeros] << n
end
end
# => {positives:[1, 2, 3, 4, 5], negatives:[], zeros:[]}
That approach is useful when a single pass needs to gather several different subsets. Instead of splitting the array multiple times, you classify each element once and keep the results organized in one object. The method can also flatten nested arrays by accumulating into a flat destination with concat.
flattening nested structures
nested = [[1, 2], [3, 4], [5, 6]]
flat = nested.each_with_object([]) { |subarr, arr| arr.concat(subarr) }
# => [1, 2, 3, 4, 5, 6]
Flattening with concat is a good reminder that the accumulator can be any object with the right behavior. Here the array is modified in place, which keeps the code compact and avoids building temporary arrays for each nested element.
with hashes
stock = { apples: 5, oranges: 3, bananas: 0 }
out_of_stock = stock.each_with_object([]) { |(k, v), arr| arr << k if v.zero? }
# => [:bananas]
# Transform a hash into another hash
prices = { apple: 1, banana: 2, cherry: 5 }
doubled = prices.each_with_object({}) { |(k, v), h| h[k] = v * 2 }
# => {:apple=>2, :banana=>4, :cherry=>10}
When the accumulator is a hash, each_with_object works especially well for lookups, grouping, and reformatting nested data. The block can keep the transformation local while the hash grows in a predictable way.
common mistakes
forgetting the return value
Unlike inject, the block return value does not matter:
# This works fine, even though block returns nil
[1, 2, 3].each_with_object([]) { |n, arr| arr << n if n.even? }
# => [2]
# The accumulator object is returned, not the block result
The return value is easy to overlook because inject and each_with_object look similar at a glance. With each_with_object, the block is about side effects on the accumulator, not about producing a replacement object.
confusing parameter order
Remember: element comes first, then the accumulator:
# Wrong - this is a common mistake
[1, 2, 3].each_with_object([]) { |arr, n| arr << n }
# => NoMethodError: undefined method `<<' for nil:NilClass
# Correct - element, then accumulator
[1, 2, 3].each_with_object([]) { |n, arr| arr << n }
# => [1, 2, 3]
performance notes
each_with_objectcreates your accumulator object once, then modifies it- Avoid creating new objects inside the block if performance matters
- For simple numeric accumulation,
inject/reduceis often clearer
This makes the method a good default for collection building, but not a universal replacement for reductions. When you are just summing numbers or combining scalars, inject can still be the more direct tool.
return value
Always returns the object you passed in, regardless of what happens in the block:
result = [1, 2, 3].each_with_object({}) { |n, h| h[n] = n * 2 }
result # => {1=>2, 2=>4, 3=>6}
# The exact same object you passed as the second argument
See Also
Array#map— transform each element into a new arrayArray#inject— accumulate values by reassigningEnumerable#group_by— group elements by a criteriaHash#each— iterate over key-value pairs