What is Enumerable?
The Enumerable mixin provides collection classes with several traversals and searching methods, and with the ability to sort. The class must provide a method each, which yields successive members of the collection.
https://ruby-doc.org/core-2.7.1/Enumerable.html
Simply, Classes which have each method like Array, Hash, and String have Enumerable methods
Find code which uses each, consider using Enumerable methods
If you see the code which uses each
, potentially you can re-write the procedure without using it. Try to look for it if you think up some parts of your codebase.
Furthermore, utilize appropriate Enumerable method makes your code sophisticate. Let's see and learn with the exact examples which I've ever seen.
Usecases
Case 1: select
Before
arr = [1, 2, 3, 4, 5]
new_arr = []
arr.each do |v|
new_arr << v if v.odd?
end
p new_arr # => [1, 3, 5]
After
new_arr = arr.select(&:odd?)
p new_arr # => [1, 3, 5]
Case 2: map
Before
arr = [1, 2, 3, 4, 5]
new_arr = []
arr.each do |v|
new_arr << v * 2
end
p new_arr # => [2, 4, 6, 8, 10]
After
new_arr = arr.map { |v| v * 2 }
p new_arr # => [2, 4, 6, 8, 10]
Case 3: inject
Before
arr = [1, 2, 3, 4, 5]
sum = 0
arr.each do |v|
sum += v
end
p sum # => 15
After
arr = [1, 2, 3, 4, 5]
sum = arr.inject(:+)
p sum # => 15
Case 4: any?
Prerequisite
booking_statuses are defined as below and intend to validate the status transition
booking_statuses = {
pending: 0,
payment_requested: 1,
paid: 2,
cancelled: 3
}
Before
def validate_booking_transition(passed_status)
if passed_status == booking_statuses[:cancelled]
allowed = [
booking_statuses[:pending],
booking_statuses[:payment_requested],
booking_statuses[:paid]
].include?(passed_status)
elsif ...
.
.
.
end
After
def validate_booking_transition(passed_status)
if passed_status == booking_statuses[:cancelled]
allowed = %i(pending payment_requested paid).any? do |v|
passed_status == booking_statuses[v]
end
elsif ...
.
.
.
end
Case 5: group_by
Before
arr = [{code: 'a', val: 1}, {code: 'a', val: 2}, {code: 'b', val: 3}, {code: 'b', val: 4}]
new_hash = {}
arr.each do |hash|
k = hash[:code]
new_hash[k] = [] if new_hash[k].nil?
new_hash[k] << hash[:val]
end
p new_hash #=> {"a"=>[1, 2], "b"=>[3, 4]}
After
new_hash = arr.group_by { |h| h[:code] }.transform_values { |grouped_arr| grouped_arr.map { |h| h[:val] } }
p new_hash #=> {"a"=>[1, 2], "b"=>[3, 4]}
Consideration
Look at each use-case. Case 1-3 are general Enumerable methods' introduction.
Case 4 shows us utilizing appropriate Enumerable methods would be able to express what we intend to.
So, what can say about Case 5? Does it look smarter than before? Or it became more complex?
Let's see more details about each case.
In the loop,
- Case 1: Extract params which fit the condition
- Case 2: Multiply 2 to each param
- Case 3: Sum up the params
- Case 4: Check there are any params which fit the condition
From that, we can say they proceed 1 procedure within the loop.
The difference of Case 5 compared with others, is it does several procedures within the iteration.
They are
- Generate a hash which has
code
as key - Generate
val
array per hash key
Furthermore, it executes map
within transform_values
method. It made the situation worse -- yes, from the Calculate order aspect, it generates a dual-loop from a single loop.
So that we can say, if we should handle several procedures within a single iteration, each
could have initiative from both Calculation Order and Readability aspects.
Re-think the pros of using Enumerable methods
We can describe our intention more clearly by appropriate Enumerable methods rather than each
.
And in many cases, readability and maintainability are the most important policy. So why not using it?
And if you aren't familiar with functional programming, it even could be a good opportunity to think about side effects.
But, as I demonstrate by Case 5, each could be the best approach after all. Make sure you do not generate nested iterations by refactoring.
Conclusion
Enumerable makes your programming fun. Enjoy!
In Japanese
Top comments (0)