Chaining Enumerables in Ruby

I’d like to share something that felt like a HUGE “light bulb moment” for me when I was learning Ruby: chaining enumerables! We can use several enumerables all on one line to perform some pretty complex actions.

Wooden puzzle pieces

Let’s say we are working with three models: Fans, Clubs, and the connecting model, ClubOaths. A Club has many Fans through ClubOaths, and a Fan belongs to a Club through a ClubOath. It is the ClubOath’s job to keep track of these connections. We can write methods for the Fan and Club classes to give us all kinds of insights, like the number of fans in each club, most popular club, youngest aged fan in each club, etc.

Keenan Cornelius using Lapel Guard in competition

For this example, let’s figure out how to find the average age of all of the Fans my favorite club, “Lapel Lovers”.

First, since ClubOaths is responsible for keeping track of these connections, we will need to find all of the ClubOaths that are attached to our club:

lapel_oaths = ClubOath.all.select { |oath| oath.club.name == "Lapel Lovers" }

Here, we are using the .select enumerable to look through every ClubOath and shovel each one into an array if our argument == true. In this case, if the ClubOath’s .club.name attribute is equal to our club (“Lapel Lovers”), that Oath gets shoveled into the array. After .select is done iterating through each ClubOath, it returns an array which contains only the ClubOaths for our club. Here, we’ve assigned that array to the variable lapel_oaths.

Next, we need to look at all of the Fans of this club. Now that we have all ClubOaths for our club, we can use a .map to find and return the Fan objects related to those ClubOaths:

lapel_fans = lapel_oaths.map { |oath| oath.fan }

Now, our lapel_fans variable is an array of all the Fans of our club. Here is an example of one of our Fan objects:

#<Fan:0x00007f86bc97c058 @age=28, @name="Keenan Cornelius">

It looks like each Fan has an age and a name. Our goal is to find the average age of all of our Fans, so we want to take the sum of all ages divided by the number of Fans.

Let’s create an array of all Fans’ ages:

all_ages = lapel_fans.map { |fan| fan.age }

Here, we’ve used .map to look at each Fan object and shovel each Fan’s age into an array, which is now stored in the all_ages variable. For this example, let’s say it looks like this:

all_ages => [30, 29, 47, 35, 47]

Now, let’s add all of the array elements up:

all_ages_sum = all_ages.inject { |sum , age | sum + age }

Using .inject, we’ve added each age to a sum variable, which starts at 0 and continues to add on top of itself on each iteration. Again, we’ve assigned this to a variable, this time named all_ages_sum.

Great — we’re almost able to find the average. We need the number of Fans of our Club to divide from the sum of their ages:

fans_count = lapel_fans.count

Now, we can finally calculate the average age of our Fans! Note that we will need to use .to_f to make sure that our return value can be a float, or a number with decimals.

lapel_fans_average_age = (all_ages_sum / fans_count).to_f

Wow, we’ve done it! It only took 15 minutes and six different variables! Now, what if I told you that you could find this all with one line:

Man smiling wearing a VR headset
ClubOath.all.select{|oath|oath.club.name=="Lapel Lovers"}.map{|oath|oath.fan.age}.inject{|sum,age|sum+age}/ClubOath.all.select{|oath|oath.club.name=="Lapel Lovers"}.map{|oath|oath.fan}.count.to_f

Ruby will run each enumerable from left to right, and it’s smart enough to know to separate each side of the / (division)! Wow, thanks Ruby.

Of course, this is a really complex line here (we’re connecting 7 enumerables and functions), and it would be more efficient to break some of this up into Class instance methods, such as a method to simply find a club’s ClubOaths, and another to find a club’s fans. However, being able to avoid using a variable for each step in your process to feed into the next step was a big level-up for me, and I hope that this gives you some ideas on how you can evolve your own coding!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Justin Langlinais

Justin Langlinais

Software Engineer and Brazilian Jiu-Jitsu brown belt