Enumerable
This chapter introduces Enumerable, a module that provides over 50 collection manipulation methods. We also discover Comparable, which allows comparing objects with each other.
Principle
We have already used .map, .select, .reject on Arrays. These are not strictly Array methods: they come from the Enumerable module that Array includes automatically.
The principle is simple: if a class defines the each method and includes Enumerable, it inherits all these methods for free. each is the only building block needed — Ruby builds everything else on top of it.
This is exactly what we learned in chapter 10 with mixins: include Enumerable injects dozens of methods into the class.
Including Enumerable in a class
class Registry
include Enumerable
def initialize
@pokemon_list = []
end
def add(pokemon)
@pokemon_list << pokemon
return self
end
# The only required method for Enumerable
def each(&block)
@pokemon_list.each(&block)
end
end
include Enumerablegives access tomap,select,sort_by,reduce, etc.eachis the contract: Enumerable uses it as the foundation for all its methods.&blockcaptures the block passed toeachand forwards it to@pokemon_list.each.return selfinaddallows chaining:registry.add(a).add(b).
Transforming: map and flat_map
map transforms each element and returns a new Array:
names = registry.map { |pokemon| pokemon.name }
# => ["Pikachu", "Charizard", "Blastoise"]
flat_map does the same thing, then flattens the result by one level. This is useful when each element produces an Array:
# Each Pokemon can have multiple types
all_types = registry.flat_map { |pokemon| pokemon.types }
# => [:electric, :fire, :flying, :water]
# Without flat_map, we would get an Array of Arrays
registry.map { |pokemon| pokemon.types }
# => [[:electric], [:fire, :flying], [:water]]
flat_map=map+flatten(1). It is a very common shortcut.
Filtering: select, reject, find
We already saw them in chapter 3, but they come from Enumerable:
# Keep Fire-type Pokemon
fire_pokemon = registry.select { |pokemon| pokemon.types.include?(:fire) }
# Exclude KO'd Pokemon
alive = registry.reject { |pokemon| pokemon.ko? }
# Find the first Water-type Pokemon
first_water = registry.find { |pokemon| pokemon.types.include?(:water) }
selectandrejectreturn an Array.findreturns a single element ornil.
Reducing: reduce
reduce (also called inject) accumulates a value while traversing the collection:
# Sum all levels
total = registry.reduce(0) { |sum, pokemon| sum + pokemon.level }
How it works, step by step:
sumstarts at0(the initial value passed toreduce)- First iteration:
sum = 0 + 25->sumis25 - Second iteration:
sum = 25 + 36->sumis61 - And so on... The final result is the last value of
sum.
There are also shortcuts:
# sum is a shortcut for reduce(0) { |s, p| s + p.level }
total = registry.sum { |pokemon| pokemon.level }
# reduce with a symbol (when working directly with numbers)
levels = [25, 36, 40]
total = levels.reduce(:+) # => 101
sumis more readable thanreducefor simple additions.reduce(:+)calls the+method between each pair of elements.
Sorting: sort_by, min_by, max_by
sort_by sorts according to a criterion extracted by the block:
# Sort by level ascending
by_level = registry.sort_by { |pokemon| pokemon.level }
# Sort by level descending (negate the value)
strongest = registry.sort_by { |pokemon| -pokemon.level }
min_by and max_by find the extreme element without sorting the entire collection:
strongest = registry.max_by { |pokemon| pokemon.level }
weakest = registry.min_by { |pokemon| pokemon.level }
# Both at once
weakest, strongest = registry.minmax_by { |pokemon| pokemon.level }
sort_byreturns a new sorted Array.min_by/max_byare more efficient thansort_by.first/sort_by.lastbecause they traverse the collection only once.
Grouping: group_by and tally
group_by organizes elements into subgroups based on a criterion:
by_type = registry.group_by { |pokemon| pokemon.types.first }
# => { :electric => [...], :fire => [...], :water => [...] }
tally counts occurrences of each value in an Array:
# Count how many Pokemon of each type
census = registry.flat_map { |pokemon| pokemon.types }.tally
# => { :electric => 2, :fire => 3, :water => 1, :flying => 1 }
group_byreturns a Hash whose keys are the values returned by the block.tallyis a very handy shortcut for counting.
Counting: count and sum
# How many Pokemon in total?
total = registry.count
# How many Fire-type Pokemon?
fire_count = registry.count { |pokemon| pokemon.types.include?(:fire) }
# Sum of all levels
total_levels = registry.sum { |pokemon| pokemon.level }
countwithout a block returns the total number. With a block, it counts those that satisfy the condition.
Predicates: any?, all?, none?
These methods return true or false:
# Is there at least one Water-type Pokemon?
registry.any? { |pokemon| pokemon.types.include?(:water) } # => true
# Are all Pokemon above level 10?
registry.all? { |pokemon| pokemon.level > 10 } # => true
# No Pokemon is KO'd?
registry.none? { |pokemon| pokemon.ko? } # => true
Slicing: take and drop
# The first 3 Pokemon
first_three = registry.take(3)
# All except the first 2
without_first_two = registry.drop(2)
Chaining calls
You can chain multiple Enumerable methods to build complex queries:
# The names of the 3 highest-level Pokemon
top_three = registry
.sort_by { |pokemon| -pokemon.level }
.take(3)
.map { |pokemon| pokemon.name }
- Each method returns an Array, which allows calling the next one on it.
- The order of operations matters: filtering before sorting is more efficient.
The Comparable module
Comparable works on the same principle as Enumerable: you include the module and define a single method, the <=> (spaceship) operator. In return, you get <, >, <=, >=, between? and clamp.
class Pokemon
include Comparable
attr_reader :name, :level
def initialize(name, level)
@name = name
@level = level
end
def <=>(other)
return @level <=> other.level
end
end
pikachu = Pokemon.new('Pikachu', 25)
charizard = Pokemon.new('Charizard', 50)
puts pikachu < charizard # => true
puts charizard > pikachu # => true
puts pikachu.between?(Pokemon.new('Min', 10), charizard) # => true
<=>returns-1ifselfis less,0if equal,1if greater.- Once
<=>is defined,.sortworks automatically without a block.
Conclusion
- Including
Enumerableand definingeachgives access to over 50 methods. maptransforms,flat_maptransforms and flattens.select/rejectfilter,findfinds the first match.reduceaccumulates a value.sumis a shortcut for additions.sort_bysorts by criterion.min_by/max_byfind the extremes.group_bygroups,tallycounts occurrences.countcounts,any?/all?/none?check conditions.- Including
Comparableand defining<=>gives<,>,<=,>=,between?,clamp. - You can chain Enumerable calls for complex queries.