Programming made easy with Ruby Enumerables

Enumerable is a mixin consists of collection classes with various methods which makes day to day operations in ruby really easy. Enumerable particularly contributes heavily in making programming with ruby super-fast and stress free. Using different methods provided by this mixin is fairly easy but learning how things happen under the hood will take you one step closer in becoming an expert developer in Ruby programming language. In this article we will learn and understand few Enumerable methods and how they work. Though this mixin has huge set of methods we will discuss three most commonly used methods in our everyday Ruby programming Map, Select and Reduce.

MAP (to transform collection)

Map enumerator returns a new array by iterating a block of code on every element. In the end we will get the new array having all the transformed values. To understand this better first lets pick some piece of code we will run our enumerable on. For this example I will be using a csv having thousands of rows of data of classic rock songs, you can find it here along with final source code for all the examples in this post https://github.com/akshatpaul/articles

Lets first read this csv file, following code will do the same and print first record from it, also show the count for number of records in this csv:

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', :headers => true, :header_converters => :symbol)

p songs.first p songs.count


Output

$ ruby songs.rb

#<CSV::Row name:"Caught Up in You" artist:".38 Special" year:"1982" combined:"Caught Up in You by .38 Special" first:"1" year:"1" playcount:"82" fg:"82"> 2229


2229 wow that’s a hige collection of records and quite a lot of information there, what if we only need artist from this plethora of data. This is one of the most common pattern of transformation of data where we convert data from collection 1 into collection 2.

Lets first write a block of code for this :

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', :headers => true, :header_converters => :symbol)

artists = [] songs.each do |song| artists << song[:artist] end
p artists.first


Output

$ ruby songs.rb
".38 Special"


What we have done here is a implemented simple block of code where we are iterating over each element from songs collection and then saving only artist information in artists array. Intear of printing first artist with artists. First you can actually check the count or print the whole array to see complete result.

This pattern is so common in Ruby that we have a name for it Map. With map method same code can be reduced to following :

artists = songs.map do |song|
    song[:artist]
end

This is a very simple example and keeping it separate line is not paying proper respect to beauty of Ruby programming language. We can actually make our code snippet inline:

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', :headers => true, :header_converters => :symbol)
artists = songs.map { |song| song[:artist] }
p artists.first


Output:

ruby songs.rb
".38 Special"


The output remain the same whether you write full definition or replace it with a Map.

The Ruby Workshop Akshat Paul

This was a very simple example but look at the ease with which we are able to remove definition with a simple and elegant inline code with Map enumerable.

Select (to filer a collection)

As the name suggest with Select enumerable we can iterate over a collection and filter out data based on certain condition. In continuation from our above example lets only pickup song records out of 2229 whose playcount is over 50. If we write a Ruby code for to get the result it will be something like this:

require 'csv'
require 'pry'
songs = CSV.read('classic-rock-song-list.csv', 
                  :headers => true, 
                  :header_converters => :symbol)
result = []
songs.each do |song|
    result << song if song[:playcount].to_i >=50
end
p result.count

Output:

$ ruby songs.rb
250

Our result is now reduced to 250 from 2229 if you try to print result array you will find complete records for songs whose playcount is 50. Lets transform this code using select :

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', 
                  :headers => true, 
                  :header_converters => :symbol)
result = songs.select do |song|
    song[:playcount].to_i >=50
end     
p result.count

Output:

$ ruby songs.rb
250

Here we have used select enumerator to get only records based on playcount condition. However this do and end are just a drag since we are working with Ruby we can definetly make it a bit clean.

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', 
                  :headers => true, 
                  :header_converters => :symbol)
result = songs.select { |song| song[:playcount].to_i >=50 }
p result.count

Output:

$ ruby songs.rb
250
The Ruby Workshop Akshat Paul

As you see the output remains same in both the cases when you write complete defiation or simply replace it with a inline Select.

Reduce (to Aggregate values)

Reduce aggregates all the elements of the collection based on a binary operation applied to it. Let’s take an example in our csv we have playcount against every song in order calculate average playcount for our list of classic rock songs we would need to sum up the playcount columns and divide by total number of records. The block of code for such an operation would be something like this:

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', 
                  :headers => true, 
                  :header_converters => :symbol)
total_playcount = 0
songs.each do |song|
    total_playtime += song[:playcount].to_i
end     
p total_playcount/songs.count

Output

Perfect, but of course we do have an enumerable for this called reduce. With reduce you get an accumulator value and an element. Based on the operation application the new values will be in accumulator. Let’s revisit above code with reduce enum :

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', 
                  :headers => true, 
                  :header_converters => :symbol)
total_playcount = songs.reduce(0) do |sum, song|
    sum + song[:playcount].to_i
end     
p total_playcount/songs.count

Output <div style=”background: #ffffff; overflow:auto;width:auto;border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;”><pre style=”margin: 0; line-height: 125%”>$ ruby songs-reduce.rb 16 </pre></div>

Here our accumulator is sum and after each iteration basedon our addition operator new value is set in sum. You also see reduce is provided with (0) this the initial value to start with. Ofcourse we must refactor and clean up above code to make it more like Ruby.

require 'csv'
songs = CSV.read('classic-rock-song-list.csv', 
                  :headers => true, 
                  :header_converters => :symbol)
total_playcount = songs.reduce(0) { |sum, song| sum + song[:playcount].to_i }
p total_playcount/songs.count

Output

$ ruby songs-reduce.rb
16
The Ruby Workshop Akshat Paul

Reduce also has an alias which is inject both works the same way and there is no performance advantage for using one over another. Can be used based on the problem solving so that code written is easy to understand.

In this article we only shared few enumerables but Ruby has a long list of methods to search, sort, traverse, transform and manipulate collections. Some of the other popular ones are group_by, sort_by, min, max, min_by, minmax_by, minmax, flat_map, reject, reverse_each, each_with_index, each_with_object and many many more. Best way to use these methods is by refactoring your code constantly and consistently, write your original definition no matter how long, lousy or cumbersome it maybe and then go through various enums which one can help you improve your code. Some time combination of few would help while some day you would stumble upon some absolutely new method making your solution more elegant. Keep exploring this swiss knife of Ruby programming language and make your code better.

The Ruby Workshop Akshat Paul

The Ruby Workshop Akshat PaulThe Ruby Workshop A Practical no-nonsense introduction to Ruby development. Learn by doing real-world development, supported by detailed step-by-step examples, screencasts and knowledge checks. Become a verified practitioner, building your credentials by completing exercises, activities and assessment checks, and get certified on completion.

Publisher : PacktPub
Release Date : November 2019
Role: Author
Complete Course : Microsite
Buy : Amazon

Share

Leave a Comment

Your email address will not be published. Required fields are marked *

Speaking at React Universe 2024