Nithin Bekal Posts About Notes

What's new in Ruby 2.7

08 Jan 2020

Ever since Ruby 2.1.0, the Ruby core team have released a new version of Ruby every Christmas, and this time we got 2.7.0. This will likely be the last 2.x release of Ruby, because Ruby 3.0 is expected to ship next Christmas.

Here’s a quick summary of some of the most interesting features in this release. (For previous versions, see: 2.6, 2.5, 2.4, 2.3).

Pattern matching

A new pattern matching syntax was introduced, which allows you to write case statements like this:

def test_pattern(value)
  case value
  in 0
    'Zero'
  in Integer if value < 0
    'Negative'
  in Integer => n
    "Number #{n}"
  in { value: x }
    "Hash value: #{x}"
  in _
    'Unknown'
  end
end

test_pattern(-1)            #=> 'Negative'
test_pattern(0)             #=> 'Zero'
test_pattern(2)             #=> 'Number: 2'
test_pattern({ value: 3 })  #=> 'Hash value: 3'
test_pattern('four')        #=> 'Unknown'

Startless ranges

In 2.6, we got end-less ranges, and this time we will have start-less ranges. This will allow syntax like:

case n
when ..0  then 'Negative'
when 1..9 then 'Single digit'
when 10.. then 'Two or more'
end

Another interesting use of this will be the ability to use this in DSLs. For instance, we could do this in ActiveRecord:

Task.where(due_on: ..Date.today)

Numbered parametes

A new syntax was added to add numbered parameters to blocks.

(1..3).map { _1 * _1 } #=> [1, 4, 9]

hash = { a: 2, b: 3 }
hash.map { "#{_1} = #{_2}" } #=> ["a = 1", "b = 2"]

add_3 = -> { _1 + _2 + _3 }
add_3.call(3, 5, 7) #=> 15

These are contrived examples, but so far I’m not convinced it was worth adding this new syntax, and I don’t see myself using this over explicit arguments.

New Enumerable and Array methods

Enumerable#tally counts the occurences of each item in an enumerable, and returns it as a hash.

strings = ['a', 'a', 'a', 'b', 'c']
strings.tally
#=> { 'a' => 3, 'b' => 1, 'c' => 1 }

# in 2.6, we'd do it like this:
strings.each_with_object({}) do |value, result|
  result[value] ||= 0
  result[value] += 1
end

Enumerable#filter_map combines select and map into a single block, avoiding the need for an intermediate array allocation.

# squares of odd numbers in 1..10
(1..10).filter_map { |x| x*x if x.odd? } #=> [1, 9, 25, 49, 81]

# ruby 2.6
(1..10).select(&:odd?).map { |x| x*x }

Array#intersection is the equivalent of calling & on arrays, but allows multiple arguments.

[1,2,3,4,5].intersection([2,3,4], [2,4,5]) #=> [2, 4]

Enumerator::Lazy#eager converts a lazy enumerator into a non-lazy one.

(1..).lazy
  .select(&:odd?)    # lazy
  .take(3)           # lazy
  .eager             # next operation will be non-lazy
  .map { |x| x * x } #=> [1, 9, 25]

Argument forwarding syntax

New syntax (...) has been added to allow forwarding all the arguments of a method.

def add(a, b)
  a + b
end

def add_and_double(...)
  add(...) * 2
end

add_and_double(2, 3) #=> 10

Deprecation behavior in keyword arguments

In previous versions of Ruby, if you had a method with keyword arguments, and it was called with a Hash as the last argument, the hash would have been converted to keyword args. However, this has been deprecated with a view to remove this behavior in 3.0. This will now cause a warning.

def foo(key: 1)
  key
end

foo({ key: 2 })
#=> 43
# warning: The last argument is used as the keyword parameter
# warning: for `foo' defined here

The splat operator can be used to convert a hash to keyword args.

foo(**{ key: 2 })

Compaction GC

The Ruby garbage collector has seen continual improvements with every release, and this time a compactor has been introduced which defragments the heap and helps reduce memory usage. A GC.compact method was also introduced, to allow manually triggering compaction.

IRB improvements

There are a lot of improvements to IRB, with features such as multiline editing, rdoc integration to show documentation in IRB and colorized output.

Although I’m happy to see improvements here, IRB still lags far behind pry, and I don’t see myself using it much until it catches up.

Calling a private method on self

The behavior of private has changed slightly. Previously, if you called a private method on self, (eg. self.foo), it would have been a NoMethodError because private methods can only be called on the implicit receiver. This has changed so that calling a private method on self no longer raises

This was previously only possible for private attr_accessors, but now has been extended to all private methods.

What features are we not getting yet?

Aside from all the new features that were added, this version is also interesting for some features that didn’t make the cut. Here’s a few that were introduced as experimental features but reverted before the final release.

The pipeline operator was introduced, but reverted based on the community’s feedback.

# with pipeline
foo
  |> bar 1, 2
  |> display

# normal method chaining
foo
  .bar(1, 2)
  .display

I felt that it was an unnecessary addition because it looks like Elixir’s pipeline operator, but effectively works like a method call. In the above example, the only difference is that the pipeline (|>) version doesn’t require parentheses around bar’s arguments, whereas it is required for the normal method call.

A method reference operator was introduced and subsequently removed. This would have allowed writing File.method(:read) as File.:read. An example that shows how this would have provided another way to chain operations:

some_uri
  .then(&Net::HTTP.:get)
  .then(&JSON.:parse)

This was reverted because Matz and the core team wanted to rethink the broader design around how functional programming features would work in Ruby.

Immutable strings are not going to be the default yet. When the frozen_string_literal: true magic comment was introduced in Ruby 2.3, the plan was to make string literals immutable by default in Ruby 3.0. However, Matz has reluctantly abandoned the idea in order to avoid compatibility issues with gems, and to make the transition to Ruby 3.0 easier.

And finally, a not about the notorious flip flop operator. This confusing feature was deprecated in 2.6, with hopes that it would be removed in 3.0. However, after a few requests from the community, Matz has decided to revert the deprecation, so it’s not going away in the foreseeable future.

Nithin Bekal
Hi, I’m Nithin Bekal. I work at Shopify in Ottawa, Canada. Previously, co-founder of CrowdStudio.in and WowMakers. Ruby is my preferred programming language, and the topic of most of my articles here, but I'm also a big fan of Elixir. Tweet to me at @nithinbekal.