What’s new in Ruby 2.7?

Guy Maliar
Ruby Inside
Published in
6 min readDec 2, 2019

With Rubyconf 2019 behind us and Ruby 2.7 releasing this December, it’s the perfect time to go through what are the new features in the newest 2.x Ruby version and also the last release before 3.0.

What defines this version of Ruby is a mix of quality-of-life improvements such as Symbol#start_with? and Enumerable#filter_map, big yet often controversial features such as pattern matching and numbered arguments and bug fixes.

What also makes this version special is that it is the last one before Ruby 3.0 that is due will be released in Dec 2020 and has been presented by Matz in this year’s Rubyconf keynotes (https://www.youtube.com/watch?v=2g9R7PUCEXo&list=PLE7tQUdRKcyZDE8nFrKaqkpd-XK4huygU&index=2&t=0s) and covered by my fellow team member, Snir David, in post https://medium.com/@snird/rubyconf-2019-main-takeaways-from-the-keynote-ruby-3-0-and-the-road-ahead-b17b42cc50be.

Pattern Matching

The best one line definition would be case/when + multiple assignments.

A more complete definition would be multiple patterns which some arbitrary data are checked against (case/when) and deconstructing the data according to those patterns (multiple assignments).

The best place to learn about pattern matching is probably the Ruby test files.

case [0, [1, 2, 3]]
in [a, [b, *c]]
p a #=> 0
p b #=> 1
p c #=> [2, 3]
end

Pattern matching checks for the structure of the data so that it can be partially deconstructed as well,

case [0, [1, 2, 3]]
in [0, [a, 2, b]]
p a #=> 1
p b #=> 3
end

Much more information is available in the following slides which I highly recommend reading.

Numbered Parameters

Numbered parameter as the default block parameter is introduced as an experimental feature, it allows us to drop the pipe definition in blocks, (|value|) and create shorter blocks with numbered parameters _1, _2, _3 etc.

[1, 2, 10].map { _1.to_s(16) } #=> ["1", "2", "a"]
  • Defining a local variable named _1 is still honored when present, but renders a warning
_1 = 0
[1].each { p _1 } # prints 0 instead of 1, but also warns.

Some concerns have been issued by the community about how clear and/or how needed this feature actually is.

From the clarity perspective it’s quite unclear (no pun intended) what is actually better.

# bad
names.map { |name| name.upcase }
# good
names.map(&:upcase)
# also good?
names.map { _1.upcase }

I’ve thought of and found some examples of what can we do with it.

# Shorter one-liners?hash = {
key_1: "1",
key_2: "2",
}
hash.map { [_1.to_s, _2.to_i] }.to_h
# => { "key_1" => 1, "key_2" => 2 }
# Simpler simple-one-lines that can't be otherwise written?[1, 2, 3].collect { _1 ** 2 }# Shorter definitions of less important variables?h = Hash.new { _1[_2] = [] }
h[:a].push(1)
# Cleaner method chains?HTTP.get(url).body.then { JSON.parse(_1, symbolize_names: true) }# Weird each with objects?[:method_1, :method_2, :method_3].each_with_object(Post.find(1)) { _2.send(_1) }

Argument Forwarding

A nice quality of life feature when we want to pass all the arguments as is,

def foo(...)
bar(...)
end

All arguments to foo are forwarded to bar, including keyword and block arguments.

Beginless Ranges

An experimental feature with still with some concerns, beginless ranges are looked more as a feature for DSL completeness rather than usability. This feature is used mostly for comparison where we’d like to define a condition for “everything below the specified value”. Dates and numbers make the perfect example and even for version strings.

case release_date
when ..1.year.ago
puts "ancient"
when 1.year.ago..3.months.ago
puts "old"
when 3.months.ago..Date.today
puts "recent"
when Date.today..
puts "upcoming"
end

ruby_version_is ..."2.6" do
# ...
end

Flip flop operator is back

A lesser known feature in Ruby is the flip flop operator. It has it’s root coming from Perl but isn’t used as much anymore.

The flip flop operator is a range (..) operator used between two conditions in a conditional statement. It evaluates to false until the first condition evaluates to true and stays true until the second condition evaluates to true.

(1..10).each { |i| puts i if (i == 5) .. (i == 8) }
5
6
7
8
=> 1..10

Array#intersection

The equivalent to:

[ 1, 1, 3, 5 ] & [ 3, 2, 1 ]                 #=> [ 1, 3 ]
[ 'a', 'b', 'b', 'z' ] & [ 'a', 'b', 'c' ] #=> [ 'a', 'b' ]

Can now be written as:

[ 1, 1, 3, 5 ].intersection([ 3, 2, 1 ])                #=> [ 1, 3 ]
[ 'a', 'b', 'b', 'z' ].intersection([ 'a', 'b', 'c' ]) #=> [ 'a', 'b' ]

Enumerable#filter_map

select and map are often used to transform an array while filtering some items out completely.

Up until now it would have been possible with

[1, 2, 3].select { |x| x.odd? }.map { |x| x.to_s }
[1, 2, 3].map { |x| x.to_s if x.odd? }.compact
[1, 2, 3].each_with_object([]) { |x, m| m.push(x.to_s) if x.odd? }

filter_map allows us to pair the two together and solve the problem faster.

[1, 2, 3].filter_map {|x| x.odd? ? x.to_s : nil } #=> ["1", "3"]

Enumerable#tally

The way to count the amount of repetitions of a specific value in an Enumerable would have been one of the followings:

array
.group_by { |v| v }
.map { |k, v| [k, v.size] }
.to_h
array
.group_by { |v| v }
.transform_values(&:size)

array.each_with_object(Hash.new(0)) { |v, h| h[v] += 1 }

As of 2.7 we can instead use tally :

["A", "B", "C", "B", "A"].tally #=> {"A"=>2, "B"=>2, "C"=>1}

Enumerator.produce

Enumerator.produce(initial, &block), will produce an infinite sequence where each next element is calculated by applying block to previous.

require 'date'dates = Enumerator.produce(Date.today, &:next) #=> infinite sequence of dates
dates.detect(&:tuesday?) #=> next tuesday
# easy Fibonacci
Enumerator.produce([0, 1]) { |f0, f1| [f1, f0 + f1] }.take(10).map(&:first)

Or for pagination (taken directly from https://bugs.ruby-lang.org/issues/14781)

require 'octokit'Octokit.stargazers('rails/rails')p Enumerator.generate(Octokit.last_response) { |response| response.rels[:next].get }
.first(3)
.flat_map(&:data)
.map { |h| h[:login] }
# => ["wycats", "brynary", "macournoyer", "topfunky", "tomtt", "jamesgolick", ...

A very short and well explanation is available here.

Symbol#start_with? / Symbol#end_with?

Just some convenience methods that behave like String#start_with? and String#end_with?

Keyword Arguments spec changes

Keyword arguments assignments spec is changing a little bit with additional warnings and deprecation warnings that will probably be hapenning next year with Ruby 3.

Taken directly from the Ruby NEWS,

  • When a method call passes a Hash at the last argument, and when it passes no keywords, and when the called method accepts keywords, a warning is emitted. To continue treating as keywords, add a double splat operator to avoid the warning and ensure correct behavior in Ruby 3.
def foo(key: 42); end; foo({key: 42})   # warned
def foo(**kw); end; foo({key: 42}) # warned
def foo(key: 42); end; foo(**{key: 42}) # OK
def foo(**kw); end; foo(**{key: 42}) # OK
  • When a method call passes keywords to a method that accepts keywords, but it does not pass enough required positional arguments, the keywords are treated as a final required positional argument, and a warning is emitted. Pass the argument as a hash instead of keywords to avoid the warning and ensure correct behavior in Ruby 3.
def foo(h, **kw); end; foo(key: 42)      # warned
def foo(h, key: 42); end; foo(key: 42) # warned
def foo(h, **kw); end; foo({key: 42}) # OK
def foo(h, key: 42); end; foo({key: 42}) # OK
  • When a method accepts specific keywords but not a keyword splat, and a hash or keywords splat is passed to the method that includes both Symbol and non-Symbol keys, the hash will continue to be split, and a warning will be emitted. You will need to update the calling code to pass separate hashes to ensure correct behavior in Ruby 3.
def foo(h={}, key: 42); end; foo("key" => 43, key: 42)   # warned
def foo(h={}, key: 42); end; foo({"key" => 43, key: 42}) # warned
def foo(h={}, key: 42); end; foo({"key" => 43}, key: 42) # OK
  • If a method does not accept keywords, and is called with keywords, the keywords are still treated as a positional hash, with no warning. This behavior will continue to work in Ruby 3.
def foo(opt={});  end; foo( key: 42 )   # OK
  • Accept non-symbols keyword arguments if method accepts arbitrary keywords
def foo(**kw); p kw; end; foo("str" => 1) #=> {"str"=>1}

Most of the examples here are either taken from my imagination or from the Ruby NEWS file, Ruby Issue Tracking System or other blog posts, if there’s any credit missing please let me know.

--

--