inject for work with collection in Ruby
- 02 October 2016
- Ruby
- inject for work with collection in Ruby
After couple of theoretical posts (Tell, Don't Ask and Law of Demeter) I want to write about more practical things.
About something that we use on everyday's job dealing with arrays and hashes. About some capabilities that Enumerable module provides us.
I really recommend you to look at that module, because it provides us such useful methods as: map
, select
, inject
, each_with_object
and so on.
In this article I would like to discuss inject
.
Enumerable#inject
This method is being used widely. Let's start from the simplest example, and then we will try to understand how it works:
[1, 2, 3, 4, 5].inject(0) { |memo, i| memo + i } # => 15
As we see from result - we calculated sum of all elements of array. How we achieved that? Let's see:
inject
goes through all elements of array- block which we pass to
inject
has two params: element of array and memo - initial value of memo passed as param to
inject
. In this case it's zero. - result of execution is written to memo for next iteration
That might sound a bit complicated, but if we go through this process step by step it will be more clear. First iteration will look like this:
# because of initial param `inject(0), memo will be equal to 0, and first element of array is 1
[1, 2, 3, 4, 5].inject(0) { |0, 1| 0 + 1 } # => 1. This time for the next iteration memo is equal to 1
# second iteration, next element of array and memo = 1
[1, 2, 3, 4, 5].inject(0) { |1, 2| 1 + 2 } # => 3. Now memo = 3
# third iteration
[1, 2, 3, 4, 5].inject(0) { |3, 3| 3 + 3 } # => 6. For the following iteration memo = 6
# and so on
I hope now it's more clear, but let's recap:
- argument, which we pass to
inject
gets the initial value for memo (you can easily change name of that variable. For example in our case we could name it accumulator) - value, that block returns is being passed as memo for the following iteration
inject
goes through all elements of collection- result of entire execution of
inject
is a last value ofmemo
Let's check more interesting example, which you can meet in day-to-day work. Let's imagine that we have an array of user's roles, and we want to get a hash where key is a role and value equals to true
.
So we need to transform this data ["user", "admin", "guest"]
to this format: {"user" => true, "admin" => true, "guest" => true}
Without inject
we could get code like this:
roles = ["user", "admin", "guest"]
roles_hash = {}
roles.each { |role| roles_hash[role] = true }
But now we know that we can write better code:
roles = ["user", "admin", "guest"]
roles_hash = roles.inject({}) { |hash, role| hash.update(role => true) }
It's usually easy to spot place where you could use inject
: if you set empty hash or array and then go through some collection and fill it in following some logic, you should use inject
instead.
Let's check more examples.
For example, you have an array of users. Each user has an attributes age
and name
. We need to get an array of names of users which age is more than 21 year. We can do that easily:
users.inject([]) do |names, user|
names << user.name if user.age > 21
names
end
Isn't it beautiful? In one small piece of code we implemented a lot of logic. We went through all users, found those which are older than 21 year, took their names and wrote them into array. I should notice that since names
should be an array, result of block should be array as well. That's why by the second line in block we returned array of names
.
Let's examine another example. Imagine we have initial value of balance, amount of payments and balance after all payments. We want to check if balance was changed properly.
Initial data:
balance_before = 1000
balance_after = 200
transactions = [200, 100, 200, 100, 50, 150]
Now you got the idea on how to do that, don't you?
balance_after == transactions.inject(balance_before) { |balance, amount| balance - amount }
One line of code! But Ruby offers us even shorter option. If all you need to do inside block is one action between element and memo, you can use short form:
balance_after == transactions.inject(balance_before, :-) # => true
It looks like "smile", but we pass method we want to call for memo
and element as a symbol. In our case it's "-".
Code from example with sum of element of array we could write like this:
[1, 2, 3, 4, 5].inject(0, :+)
We should remember about this short form and use when it's possible. But you should remember about readability as well.
I really recommend you to use inject
where it makes sense. At the beginning it might look a bit complicated, but you'll get used to it. It will allow you to solve relatively complex tasks in one or two lines of code.
PS: Ruby usually has couple names for the same method. There is an alias for inject
- it's reduce
which many people use. So if you see reduce
, just know that it's inject
.
Good luck!