The letter A styled as Alchemists logo. lchemists Syndication Icon

Putin's War on Ukraine - Watch President Zelenskyy's speech and help Ukraine fight against the senseless cruelty of a dictator!

Published January 2, 2022 Updated March 18, 2022
Cover
Ruby 3.1.0

As is yearly tradition, a new release of Ruby arrived on Christmas Day. This year we get 3.1.0 as a minor release versus last year’s 3.0.0 major release. Funny enough, I happened to wake up ~20 minutes after the minor version had been released Christmas morning. I think it was 6:20am when I started installing it. Always my favorite present to open on the holiday.

While this article won’t be comprehensive of all the new Ruby 3.1.0 features, I do want to share some insights that might be of interest.

Highlights

These are a few top level highlights but, for a complete list, see the excellent Ruby 3.1.0 Version Notes for further details.

  • YJIT: New JIT support has been added but only as an experimental feature and only works on x86 platforms which, sadly, doesn’t help those who are Apple M1 machines. That said, it is worth watching this space more.

  • Debug: This gem was not bundled as part of Ruby, originally, but is now bundled as part of Ruby 3.1.0 and is a nice upgrade to the Pry gem of years past.

  • IRB: IRB now has auto-complete and documentation support. Had you been upgrading your system prior to the 3.1.0 release, you might have been surprised and delighted by the new functionality. Definitely makes working in IRB or your project’s console a more enjoyable experience. For a video of this, see the release notes for details.

  • RBS: RBS has added a manifest.yml for loading the signatures of dependent gems for use with RBS, Steep, and TypeProf.

  • Class: You can now ask a class for it’s subclasses.

  • Integer: The #try_convert method has been added which brings it more in line with behavior found in the String, Array, Hash, and Regexp objects.

  • MatchData: #match and match_length methods have been added.

  • Array: The #intersect? methods has been added so you can ask if there are common elements or not.

  • Enumerable: The #tally method can now accept an optional hash when counting occurrences. Additionally, compact support was added too.

Punning

In the past, when defining hashes or passing keyword arguments to methods, we’d have to verbosely type {x: x, y: y} but now can accomplish the same thing by only using {:x, :y}. The flipping of the colon and dropping of the value takes a little getting used to but is quite nice in practice. This is great for hashes but is also useful with keyword arguments. For example, let’s say you have a loader that can inject a client. You can now write the implementation this way:

client = Client.new
loader = Loader.new client:

If you are like me and love only using parentheses when necessary, this is quite elegant. There is a caveat, though. Should you use punning over multiple lines, you’ll need to add parentheses to all but the last statement. Example:

client = Client.new
loader_a = Loader.new(client:)  # <= Required for the parser to not think `loader_b` is an argument.
loader_b = Loader.new client:

That is ugly but necessary. It’s also got some quirks which you can read about more in this Ruby Issue. If using RuboCop, there is an outstanding issue with the Style/MethodCallWithArgsParentheses cop which you can read about more in this issue. Thanks Koichi for the help on this! 🙇🏻‍♂️

Anonymous Block Forwarding

Along the lines of punning, you no longer have to type out block variables when forwarding. For example, consider this snippet from the Refinements gem:

def fetch_value key, *default_value, &block
  fetch(key, *default_value, &block) || default_value.first
end

The above can now be written as:

def fetch_value(key, *default_value, &)
  fetch(key, *default_value, &) || default_value.first
end

Unfortunately, due to the ambiguity of the trailing &, the Ruby parser doesn’t know if you are planning to define a variable or not so you have to wrap the method signature in parenthesis. That is a bummer but you have to use parenthesis with endless methods regardless so maybe this is fine when rewritten further as an endless method:

def fetch_value(key, *default_value, &) = fetch(key, *default_value, &) || default_value.first

Initially, there was a RuboCop issue but has been fixed thanks to Koichi’s quick response.

Pattern Matching

Standalone syntax is no longer experimental. This means you can write the following code without warnings from Ruby:

Assignment

{character: {first_name: "Malcolm", last_name: "Reynolds"}} => {character: {last_name:}}
puts "Last Name: #{last_name}"

# => Last Name: Reynolds

Boolean

basket = [{kind: "apple", quantity: 1}, {kind: "peach", quantity: 5}]
basket.any? { |fruit| fruit in {kind: /app/} }

# => true

For more on pattern patching, you might enjoy my related talk where I dive into the specifics.

Multiple Assignment Evaluation

This is only briefly mentioned in the release notes but worth emphasizing the difference between 3.0.0 and 3.1.0:

# Ruby 3.0.0 evaluation was computed in this order: berry, twig, berries, and twigs.
berries[0], twigs[0] = berry, twig

# Ruby 3.1.0 evaluation is now computed in this order: berries, twigs, berry, and twig.
berries[0], twigs[0] = berry, twig

The 3.1.0 change is much easier to reason about.

Endless Method Parenthesis

When endless methods were introduced in Ruby 3.0.0, we were forced to use parenthesis in the methods bodies. With Ruby 3.1.0, that requirement has been resolved which reduces typing but also makes refactoring between endless and standard methods much easier.

# Ruby 3.0.0 parser required parenthesis or you'd get a parser error.
def call(path) = Pathname(path)

# Ruby 3.1.0 allows optional use of parenthesis. 🎉
def call(path) = Pathname path

Upgrade

Upgrading from Ruby 3.0.0 to 3.1.0 has been fairly straight forward. Even better, RuboCop's auto corrections made the process fairly painless. I did have to make temporary tweaks to the Caliber project until the Style/MethodCallWithArgsParentheses issue is resolved but otherwise it’s been a fairly smooth experience. Almost all projects on this site are fully upgraded and using Ruby 3.1.0. 🎉

Caveats

Not everything is sunshine and roses, sadly. There are a few caveats you’ll want to be aware of:

  • Strings: Initially, there was a performance issue with String#lines but looks to be resolved but will require a patch to Ruby eventually.

  • Pathnames: There is an issue with use of File::FNM_DOTMATCH in globs which no longer answers a .. pathname. This was included in 3.0.3 but missing in 3.1.0. From what I can tell, I think this meant to be new and expected behavior.

  • Nokogiri/Bundler: There is a Nokogiri issue with installation failure and looks like Bundler requires a fix to resolve this.

  • Rails: There is issue with 7.0.0 where you’ll get a Rails::Engine is abstract, you cannot instantiate it directly. error which will prevent you from using Rails 7.0.0 with Ruby 3.1.0. I’ve not been able to release new versions of my Rails Engines because of this so am waiting for this to be resolved before continuing.

Conclusion

Definitely enjoying Ruby 3.1.0 but also am not 100% fully converted over. I’d say I’m about 80% complete, pending the caveats mentioned above.