each_with_object as an alternative to inject

Subscribe to receive new articles. No spam. Quality content.

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.

Subscribe to receive new articles. No spam. Quality content.

Comments