Skip to content

Instantly share code, notes, and snippets.

@baweaver
Created February 10, 2021 08:20
Show Gist options
  • Save baweaver/992b51e43dd05c363b4ddea55eb08f3e to your computer and use it in GitHub Desktop.
Save baweaver/992b51e43dd05c363b4ddea55eb08f3e to your computer and use it in GitHub Desktop.

Introduction

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.

Difficulty

Foundational

Some knowledge required of functions in Ruby. This post focuses on foundational and fundamental knowledge for Ruby programmers.

Prerequisite Reading:

Enumerable

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.

Cards in a Hand

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.

Remembering our Card

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.

Creating a Hand

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.

Note on Aliases

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.

Note on Syntax

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.

Transforming

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.

Predicate

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.

Search and Filter

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.

Sorting

TODO

TODO

Comparing

TODO

TODO

TODO

TODO

TODO

TODO

Counting

TODO

TODO

TODO

Grouping

TODO

TODO

TODO

TODO

TODO

TODO

TODO

TODO

TODO

Combining

TODO

TODO

TODO

TODO

TODO

Iterating and Taking

TODO

TODO

TODO

TODO

TODO

Coercion

TODO

TODO

Lazy

TODO

Wrapping Up

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment