My Favorite Features in Ruby 2.7

January 20, 2020

Every Christmas Day the Ruby core team releases a new version of Ruby and this past December 25th was no exception when the team made Ruby 2.7 available. I finally found some time to upgrade and here are some of my favorite new features of the language and IRB.

Upgrading to Ruby 2.7

I use rbenv to maintain my Ruby environment and Homebrew as a package manager for macOS so upgrading is as easy as running these two commands:

$ brew upgrade rbenv ruby-build

$ rbenv install 2.7.0

IRB Updates

The first thing you’ll notice after upgrading is that IRB looks a lot different thanks to syntax highlighting. The colored variable names, methods, and conditional operators make it much easier to visually parse code than it was in the black-and-white days.

Ruby 2.7 also has auto indentation so methods and conditional code automatically align as you type. They’ve also added auto complete so you can quickly tab out classes and methods. But my favorite feature is probably being able to load a whole method definition or class simply by pressing the up arrow key once. Prior to 2.7 it often took dozens of up arrow keypresses to load the code you were looking for:

IRB Method Recall

Array#intersection

#intersection is called on an array and takes other arrays as arguments. It returns another array that contains only the values that are common to all of the arrays. The order of the elements in the returned array is preserved from the array #intersection was called on.

array1 = [3,4,2,6,7,12,1]
array2 = [5,8,3,2,1,5,14]
array3 = [6,2,9,3,5,23,1]

array1.intersection(array2, array3)
=> [3, 2, 1]

Prior to 2.7 you could call array1 & array2 & array3 to get the same result but I think having a method that takes arguments is more readable. In terms of performance, the #intersection method is on par with array &.

Benchmark
# macOS 10.14.4 2.6 GHz i5

array1 = Array.new(1000) { rand(1...99) }
array2 = Array.new(1000) { rand(1...99) }
array3 = Array.new(1000) { rand(1...99) }

Benchmark.bmbm do |x|
  x.report("array_&") {50000.times { array1 & array2 & array3 }}
  x.report("intersection") {50000.times { array1.intersection(array2, array3) }}
end
                user     system    total      real
array_&       3.483466  0.007528  3.490994  (3.499241)
intersection  3.551033  0.015492  3.566525  (3.576167)

Enumerable#filter_map

#filter_map runs a block once on each element in an enumerable, simplifying the process of generating mapped arrays. In older versions of Ruby, a common approach to mapping arrays was to combine #map with #select or #compact. Not only is the new #filter_map method faster than the other approaches it’s also less verbose and easier to read.

Benchmark
# macOS 10.14.4 2.6 GHz i5

enum = 1.upto(1_000)

Benchmark.bmbm do |x|
  x.report("select + map") { 50000.times {
      enum.select {|i| i % 3 == 0}.map{|i| i +1 }}}
  x.report("map + compact") { 50000.times {
      enum.map {|i| i + 1 if i % 3 == 0}.compact }}
  x.report("filter_map") { 50000.times {
      enum.filter_map {|i| i + 1 if i % 3 == 0 }}}
end

                 user     system    total       real
select + map   5.655839  0.014081  5.669920  (5.684055)
map + compact  5.561898  0.039747  5.601645  (5.639864)
filter_map     5.043501  0.014836  5.058337  (5.080673)

Enumerable#tally

#tally counts how many times the same elements appear in a collection and returns a hash where the keys are the elements from the original collection and the values are how many times each element occurs:

[1,2,45,3,2,1,1,1,2,3,45,2].tally
=> {1=>4, 2=>4, 45=>2, 3=>2}

Prior to 2.7 you had to do something like this:

[1,2,45,3,2,1,1,1,2,3,45,2].group_by { |i| i }.transform_values(&:size)
=> {1=>4, 2=>4, 45=>2, 3=>2}

Along with being much easier to read, #tally appears to be significantly more performant than combining #group_by and #transform_values:

Benchmark
# macOS 10.14.4 2.6 GHz i5

array = Array.new(1000) { rand(1...99) }

Benchmark.bmbm do |x|
  x.report("group_by + trans_vals") { 50000.times {
      array.group_by { |i| i }.transform_values(&:size) }}
  x.report("tally") { 50000.times { array.tally }}
end

                         user     system     total     real
group_by + trans_vals  6.030849  0.012908  6.043757 (6.062187)
tally                  3.374915  0.010697  3.385612 (3.396156)

Wrapping Up

The new methods and updates to IRB in Ruby 2.7 share a common theme: they continue to make Ruby a lot of fun to write without sacrificing performance. Sure, the methods I mentioned above contain some magic by abstracting some details from the programmer, but that’s just fine to me if it helps me deliver value to customers while enhacing my passion for the process.


Ryan McMahon

Hi, I’m Ryan McMahon—a software developer who lives and works in Buffalo, NY. I build things for the web using React, Ruby, Rails, and .NET. Connect with me on Twitter.