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
endThere'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) }
endStarting 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 Arrays of Arrays.
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? }
# => falseWe 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)
# => trueall? 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?
# => trueall? 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?
# => falseany? 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)
# => truenone? will return true on an empty collection:
[].none?
# => trueBe 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)
# => trueIt has some interesting behavior when used without an argument on empty or single element collections:
[].one?
# => false
[1].one?
# => trueone? 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)
# => trueIt 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 }
# => nilOddly it takes a single argument, something that responds to call, as a default:
[1, 2, 3].find(-> { 1 }) { |v| v == 5 }
# => 1I 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 }
# => 1Interestingly 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