Enumerable
. Debatably one of, if not the, most powerful features in Ruby. As a majority of your time in programming is dealing with collections of items it's no surprise how frequently you'll see it used.
Foundational
Some knowledge required of functions in Ruby. This post focuses on foundational and fundamental knowledge for Ruby programmers.
Prerequisite Reading:
- Understanding Ruby - Blocks, Procs, and Lambdas
- Understanding Ruby - to_proc and Function Interfaces
- Understanding Ruby - Triple Equals
- Understanding Ruby - Comparable
Enumerable
is an interface module that contains several methods for working with collections. Many Ruby classes implement the Enumerable
interface that look like collections. Chances are if it has an each
method it supports Enumerable
, and because of that it's quite ubiquitous in Ruby.
So how are we going to cover such a large piece of the language? Categorically, and of course after we show how you can implement one of your own
Note: This idea was partially inspired by Lamar Burdette's recent work on Ruby documentation, but takes its own direction.
To start with, how do we implement Enumerable
ourselves? Via an each
method and including the module, much like Comparable
from the last post. We'll be reexploring our Card
class from that article as well as making a Hand
to contain those cards.
Let's start with our Card
class from last time:
class Card
include Comparable
SUITS = %w(S H D C).freeze
RANKS = %w(2 3 4 5 6 7 8 9 10 J Q K A).freeze
RANKS_SCORES = RANKS.each_with_index.to_h
include Comparable
attr_reader :suit, :rank
def initialize(suit, rank)
@suit = suit
@rank = rank
end
def self.from_str(s) = new(s[0], s[1..])
def to_s() = "#{@suit}#{@rank}"
def <=>(other) = precedence <=> other.precedence
end
There's one new method here for convenience that gives us a Card
from a String
, letting us do this:
Card.from_str('SA')
That gets to be handy when we want an entire hand in a second.
Now let's take a look at a Hand class that might contain these cards:
class Hand
include Enumerable
attr_reader :cards
def initialize(*cards)
@cards = cards.sort
end
def self.from_str(s) = new(*s.split(/[ ,]+/).map { Card.from_str(_1) })
def to_s() = @cards.map(&:to_s).join(', ')
def each(&fn) = @cards.each { |card| fn.call(card) }
end
Starting with Enumerable
features, we define an each
method at the bottom which takes a Block Function and calls it with each card from the cards in our Hand
.
Next we have a utility function like Card
had which allows us to make a Hand
from a String
, because otherwise that's a lot of typing:
royal_flush = Hand.from_str('S10, SJ, SQ, SK, SA')
With the above Enumerable code we can now use any of the Enumerable
methods against it:
royal_flush.reject { |c| c <= Card.from_str('SQ') }.join(', ')
# => "SK, SA"
Nifty! Now with that down let's take a look at all of the shiny fun things in Enumerable
. We'll be using more generic examples from here on out.
Ruby has many aliases, like collect
is an alias for map
. As I prefer map
I will be using that for examples. When you see a /
in the header in other sections, the first item will be the preference I will draw from, but you could use the other name to the same effect.
You might see #method_name
or .method_name
mentioned in Ruby on occasion. This means Instance Method and Class Method respectively. You might also see it like Enumerable#map
, which means map
is an Instance Method of Enumerable
.
map
expresses the idea of transforming a collection using a function, or by using the english word expressing a way to get from point A to point B. Amusingly in some functional programming languages this is expressed A -> B
, wherein ->
is the function.
For us it might be used something like this:
[1, 2, 3].map { |v| v * 2 }
# => [2, 4, 6]
In which the function is to double every element of a collection, giving us back a brand new collection in which all elements are doubles of the original.
Using the syntax for Symbol#to_proc
we can also use map
to extract values out of objects:
people.map(&:name)
If we had an Array
of people we could use map
to get all of their names using this shorthand.
map
is great for transforming collections and pulling things out of a collection.
flat_map
will both map
a collection and afterwards flatten
it:
hands = [
Hand.from_str('S2, S3, S4'),
Hand.from_str('S3, S4, S5'),
Hand.from_str('S4, S5')
]
hands.flat_map(&:cards).map(&:to_s).join(', ')
# => "S2, S3, S4, S3, S4, S5, S4, S5"
flat_map
is great when you want to extract something like an Array
from items and combine them all into one Array
. It's also great for generating products, but remember that Ruby also has the Array#product
method which works better unless you have something more involved to do.
It's for when you want one Array
rather than Array
s of Array
s.
filter_map
is interesting in that it combines the idea of filter
and the idea of map
. If the function passed to filter_map
returns something falsy (false
or nil
) it won't be present in the returned collection:
[1, 2, 3].filter_map { |v| v * 2 if v.odd? }
# => [2, 6]
In this case 2
will be ignored. filter_map
is great if you find yourself using map
, returning nil
, and using compact
at the end to drop nil
values.
This method is great when you want to both filter down a collection and do something with those values.
all?
is a predicate method, meaning it's boolean or truthy in nature. For all?
it checks all items in a collection meet a certain condition:
[1, 2, 3].all? { |v| v.even? }
# => false
We can also use shorthand here:
[1, 2, 3].all?(&:even?)
...and interestingly it also accepts a pattern, or rather something that responds to ===
:
[1, 2, 3].all?(Numeric)
# => true
all?
will also stop searching if it finds any element which does not match the condition.
An interesting behavior is that it will return true
on empty collections:
[].all?
# => true
all?
is great when you want to check if all of a collections items meet a condition, or perhaps many.
any?
is very similar to all?
except in that it checks if any of the items in a collection match the condition:
[1, 'a', :b].any?(Numeric)
Interestingly as soon as it finds a value that matches it will stop searching. After all, why bother? It found what it wanted, and it's way more efficient to say return true
rather than go through the rest.
With an empty collection any?
will return false
as there are no elements in it:
[].any?
# => false
any?
is great for checking if anything in a collection matches a condition.
none?
can be thought of as the opposite of all?
, or maybe even as not any?
. It checks that none of the elements in a collection match a certain condition:
[1, 'a', :b].none?(Float)
# => true
none?
will return true
on an empty collection:
[].none?
# => true
Be careful, as this behavior is very similar to all?
which also returns true
.
none?
can be great for ensuring that nothing in a collection matches a negative set of rules, like simple validations.
one?
is very much like any?
except in it will search the entire collection to make sure there's one and only one element that matches the condition:
[1, :a, 2].one?(Symbol)
# => true
[1, :a, 2].one?(Numeric)
# => true
It has some interesting behavior when used without an argument on empty or single element collections:
[].one?
# => false
[1].one?
# => true
one?
is great when you want to ensure one and only one element of a collection matches a condition. I have not quite had a chance to use this myself, but can see how it would be handy.
include?
checks if a collection includes a value:
[1, 2, 3].include?(2)
# => true
It has an alias in member?
.
include?
will compare all elements via ==
to see if any match the one we're looking for.
find
is how you find one element in a collection:
[1, 2, 3].find { |v| v == 2 }
# => 2
[1, 2, 3].find { |v| v == 5 }
# => nil
Oddly it takes a single argument, something that responds to call
, as a default:
[1, 2, 3].find(-> { 1 }) { |v| v == 5 }
# => 1
I honestly do not understand this myself as you cannot give it a value like this:
[1, 2, 3].find(1) { |v| v == 5 }
# NoMethodError (undefined method `call' for 1:Integer)
There is currently a bug tracker issue open against this, but it hasn't seen updates in a fair amount of time not including my recent question on it.
find
is useful for finding a single value in a collection and returning it as soon as it finds it, rather than using something like select.first
which would iterate all elements.
find_index
is very similar to find
except that it finds the index of the item rather than returning the actual item:
[1, 2, 3].find_index { |v| v == 2 }
# => 1
Interestingly it takes an argument rather than a block for a value to search for:
[1, 2, 3].find_index(3)
# => 2
...which makes a bit more sense than a default argument like in the case of find
, but to change those would break all types of potential code.
I have not found a direct use for find_index
at this point, and cases where I would use it I tend to reach for slicing and partitioning methods instead.
select
is a method with a lot of aliases in find_all
and filter
. If you come from Javascript filter
might be more comfortable, and with the introduction of filter_map
it may see more popularity. select
is more common in general usage.
select
is used to get all elements in a collection that match a condition:
[1, 2, 3, 4, 5].select(&:even?)
# => [2, 4]
Currently it uses a Block Function to check each element.
select
is typically used and great for filtering lists by a positive condition.
reject
, however, is great for negative conditions like everything except Numeric
entries:
[1, 'a', 2, :b, 3, []].reject { |v| v.is_a?(Numeric) }
# => ["a", :b, []]
Though in this particular case I would likely use grep_v
instead which we'll cover in a moment. grep
right below will have some additional insights on this distinction.
Often times Ruby methods will have a dual that does the opposite. select
and reject
, all?
and none?
, the list goes on. Chances are there's an opposite method out there.
reject
has many of the same uses as select
except that it inverts the condition and instead rejects elements which match a condition.
grep
is interesting in that it's based on Unix's grep
command, but in Ruby it takes something that responds to ===
as an argument:
[1, :a, 2, :b].grep(Symbol)
# => [:a, :b]
It behaves similarly to select
, and there are tickets out to consider adding the ===
behavior to select
, similarly with reject
and grep_v
.
Where it differs is that its block does something different:
[1, :a, 2, :b].grep(Numeric) { |v| v + 1 }
# => [2, 3]
It acts very much like map
for any elements which matched the condition.
grep
behaves mildly similarly to filter_map
except that every element in the block has already been filtered via ===
. When you need more power for conditional checking if an element belongs in the new list use filter_map
, otherwise grep
makes a lot of sense.
grep_v
is the dual of grep
, similar to select
and reject
. grep_v
behaves similarly to reject
except it uses grep
's style:
[1, :a, 2, :b].grep_v(Symbol)
# => [1, 2]
[1, :a, 2, :b].grep_v(Symbol) { |v| v + 1 }
# => [2, 3]
Just as with reject
it makes sense in cases where you want the opposite data from grep
but still want the same condition.
TODO
TODO
uniq
will get all unique items in a collection:
[1, 2, 3, 1, 1, 2].uniq
# => [1, 2, 3]
It also takes a block to let you decide exactly what criteria you want the new collection to be unique by:
(1..10).uniq { |v| v % 5 }
# => [1, 2, 3, 4, 5]
Which can be very useful for unique sizes, names, or other criteria. In the above example we're doing something a bit unique in searching for remainders from modulo which can be very useful in certain algorithmic problems.
uniq
is great when you want to get a unique collection of elements, but if you find yourself using uniq
a lot you may want to consider using a Set
instead, which we'll cover in a later article.
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
TODO
Want to keep up to date on what I'm writing and working on? Take a look at my new newsletter: The Lapidary Lemur