Nithin Bekal About

What's new in Ruby 2.6

19 Jan 2019

Ruby 2.6 was released on Christmas day, and brought a few interesting new features. Here’s a quick summary of what’s changed. As with my summaries of previous versions (2.5, 2.4, 2.3) I’m only summarizing features that I find most interesting. For a complete list of changes, take a look at the changelog.

Endless ranges

A new syntax was introduced to represent an endless range. This will be useful when matching ranges in case statements.

case n
when 1..9 then 'Single digit'
when 10..99 then 'Two digit'
when 100.. then 'Three or more'
end

Previously, (1..) would be represented as (1..Float::INFINITY). Not many people are familiar that you can represent infinity like this, so this syntax is a definite improvement.

Composing procs, lambdas and methods

#>> and #<< methods were added to Proc and Method objects to allow composing them into a new method.

square = -> (n) { n * n }
add_2  = -> (n) { n + 2 }

(square >> add_2).call(5) #=> 27
(square << add_2).call(5) #=> 49

With the #>> operation, the number gets squared first and then add_2 is performed on the result of the first proc call. In case of #<<, the order in which the procs are called is reversed.

I wish we didn’t have two different ways of composing procs, and only had #>>. But composition is an important addition to Ruby that will allow us to write in a more functional style.

Kernel#then

This isn’t a new feature, but an alias for Kernel#yield_self which was introduced in Ruby 2.5, and allows us to chain operations into pipelines like this:

isbn = '978-1-93778-549-9'

isbn.gsub('-', '')
  .then { |isbn| URI("#{API_URL}?q=isbn:#{isbn}") }
  .then { |uri| Net:HTTP.get(uri) }
  .then { |json_response| JSON.parse(json_response) }
  .then { |response| response.dig('items', 'volumeInfo') }

(Shameless plug: I recorded a guest episode on RubyTapas showing a series of refactors leading to the code above - watch it here.)

Enumerable#chain and Enumerator#+

This returns an Enumerator::Chain object, which works as a single enumerator.

(1..3).chain((5..7), [9, 10]).to_a
#=> [1, 2, 3, 5, 6, 7, 9, 10]

list = (1..3).each + (5..7) + (9..10)
list.to_a
#=> [1, 2, 3, 5, 6, 7, 9, 10]

Merge multiple hashes

Hash#merge only took one argument till 2.5. The only way to merge more than two hashes was to chain calls to #merge. You can now pass multiple hashes to the method.

foo = { a: 1 }
bar = { b: 2 }
baz = { c: 3 }

foo.merge(bar, baz) #=> { a: 1, b: 2, c: 3 }

# ruby 2.5
# foo.merge(bar).merge(baz)

Array#union and Array#difference

These methods work just like the | and & operators. The difference is that they can accept multiple arguments and are easier to chain.

a = [1, 1, 2, 3]
b = [3, 4]

a.union(b) #=> [1, 2, 3, 4]
a.difference(b) #=> [1, 1, 2]

Enumerable#to_h with block

Enumerable#to_h now accepts a block that maps keys to values. This allows us to transform hashes without creating an intermediate array using map, or using the harder-to-read reduce syntax.

hash = { foo: 2, bar: 3 }
hash.to_h { |k, v| [k.upcase, v*v] } #=> { FOO: 4, BAR: 9 }

# ruby 2.5:
# hash.map { |k, v| [k.upcase, v*v] }.to_h
# hash.reduce({}) { |result, (k, v)| result.merge(k.upcase => v) }

Enumerator::ArithmeticSequence

This subclass of Enumerator was introduced to represent objects created by calling step on Range and Numeric objects. This makes it possible to do things like check equality of sequences.

r1 = (1..10).step(2)
r2 = (1..10).step(2)

r1 == r2 #=> true if 2.6, false in 2.5
1.step == (1..).step #=> true

Changes in Range behavior

The % operator has been added as an alias to step. So (1..10) % 2 is equivalent to (1..10).step(2).

Range#=== now uses uses #cover? instead of #include?. case statements internally use ===, so this could lead to subtly different behavior in case statements.

(a..b).cover?(x) checks if a <= x < b, whereas (a..b).include?(x) iterates through the range and checks if any element equals x. This allows us to do things like:

case DateTime.now
when Date.today..Date.today+1
  puts 'matched'
else
  puts 'not matched'
end

Note: In a previous version of this article, I used a string range in the example. For backwards compatibility, the behavior of string ranges hasn’t changed.

('a'..'c').cover?('bb')   #=> true
('a'..'c').include?('bb') #=> false

case 'bb'
when 'a'..'c' then 'matched'
else 'not matched'
end
#=> 'not matched'

exception keyword argument for Kernel methods

An exception keyword argument was added for some Kernel methods like Integer, Float and system. For the Numeric methods, we can now pass exception: false to avoid raising when parsing an invalid value, and to instead return nil.

Kernel#system, on the other hand, accepts exception: true to raise if the command exits with non-zero exit status (instead of returning false) or if command execution fails (instead of returning nil).

Integer('foo', exception: false) #=> nil
Float('foo', exception: false) #=> nil

system('foo', exception: true) #=> exception

Performance improvements

The biggest news on the performance front is that a JIT implementation, called MJIT, was merged into Ruby. This can be enabled using the --jit flag.

MJIT has led to speedups in micro-benchmarks, but isn’t mature enough yet to work for larger codebases like Rails apps. It is even slower for Rails than the non-JIT version.

However, this is an important step towards the Ruby 3x3 goal, which is to make Ruby 3 at least 3x faster that Ruby 2.0. Currently Ruby 2.6 with JIT is nearly 2.5x faster than 2.0 on the Optcarrot benchmark, which is used to compare the performance of Ruby releases for the 3x3 goal.

Aside from this, a second garbage collection heap, called Transient Heap, was introduced, which reduces memory usage and improves GC speed of short lived objects. Proc.call and block.call

Better introspection

Ruby’s introspection abilities have been improved, which the introduction of the RubyVM::AbstractSyntaxTree class, which lets us parse Ruby code into AST. Binding.source_location was also added, which returns an array containing the file name and the line number where it was called.

Other changes

  • The flip flop operator is finally deprecated, and will probably be removed in Ruby 3.
  • Bundler is now a default gem, so you no longer have to do gem install bundler after installing Ruby. Rubygems 3.0.1 was also merged into Ruby.
  • Array#filter was added as an alias for #select.
  • Previous versions of ruby warned if you had an else clause in a begin block without a rescue. This will cause a syntax error in 2.6.
begin
  puts 'begin'
else
  puts 'else'
end

# Ruby 2.6
#=> foo.rb:5: else without rescue is useless

# Ruby 2.5
#=> foo.rb:7: warning: else without rescue is useless
#=> begin
#=> else

This article is part of the What's New in Ruby series. To read about a different version of Ruby, pick the version here:

2.3    2.4    2.5    2.6    2.7    3.0    3.1    3.2    3.3

Hi, I’m Nithin! This is my blog about programming. Ruby is my programming language of choice and the topic of most of my articles here, but I occasionally also write about Elixir, and sometimes about the books I read. You can use the atom feed if you wish to subscribe to this blog or follow me on Mastodon.