each_with_object as an alternative to inject
- 28 December 2016
- Ruby
- each_with_object as an alternative to inject
Recently I described how to use inject
to solve relatively complex tasks in one-two lines of code.
Today I want to describe another useful method from Enumerable
module - each_with_object
.
each_with_object
is similar to inject
, but has one significant difference. If you haven't read a post about inject - I highly recommend to do that first.
So, in case of inject
, result of block execution is being passed as memo
for the next iteration. Let's see example:
roles = ["user", "admin", "guest"]
roles_hash = roles.inject({}) { |hash, role| hash.update(role => true) }
We used method update
, which returns hash with added pair of key-value:
{}.update(foo: "bar") # => {:foo=>"bar"}
If we do it this way:
roles = ["user", "admin", "guest"]
roles_hash = roles.inject({}) { |hash, role| hash[role] = true }
We would get an error:
# NoMethodError: undefined method `[]=' for true:TrueClass
That's because result of hash[role] = true
is equals to true
. And that's what will be passed as memo
to the next iteration. We expect hash to be there, but in fact have true. That's why we have an exception.
Let's get back to example from previous article where we select users that older than 21 year:
users.reduce([]) do |names, user|
names << user.name if user.age > 21
names
end
Here we have situation when we have to return array names
from block, because result of block execution should be passed as names
to next iteration.
For such cases each_with_object
is the best option. It goes through entire collection, gets initial value as an argument and has two arguments in block: memo
and element of a collection. It also returns memo
at the end of execution.
The main difference from inject
that it doesn't return result of block execution to the next iteration. memo
is being passed to each iteration without any dependancy on block. It keeps its state during all iterations.
Using each_with_object
, we could rewrite example with roles in this way:
roles = ["user", "admin", "guest"]
roles_hash = roles.each_with_object({}) { |role, memo| memo[role] = true }
Since we don't need to return memo
expliciltly from block to be passed to next iteration - we just change memo
(hash) as we usually do. And because memo
keeps its state during all cycle - next iteration will see those changes.
Notice, that it's easy to remember order of arguments for methods like each_with_index
, each_with_object
: first goes each
- element of collection, and index
or object
goes as the second argument:
[1, 2, 3].each_with_index { |element, index| .... }
[1, 2, 3].each_with_object { |element, object| .... }
Let's write second example with selecting users older than 21 year:
users.each_with_object([]) do |user, names|
names << user.name if user.age > 21
end
In this case we have the same approach. We got rid from explicit returning of names
. Array of names
is present in each iteration. It doesn't depend on returned value from block.
To sum up: inject
fits perfectly if your code inside block returns memo
which should go to the next iteration. If you need to return memo
explicitly from block - each_with_object
fits better.