The letter A styled as Alchemists logo. lchemists
Published December 25, 2024 Updated January 19, 2025
Cover
Ruby 3.4.0

Happy Holidays. ❄️ I hope you are enjoying the new Ruby 3.4.0 release! 🎄

Once again — as is tradition every year — a new release of Ruby has arrived along with a few enhancements over last year’s Ruby 3.3.0 release. The following is a capture of highlights but definitely dig into the release notes for complete details. Enjoy!

Syntax

  • Use of the frozen_string_literal pragma is finally going to be removed with the goal of making all strings frozen by default in Ruby 3.4.0. You still need to use the pragma in your existing code, though. Check out Xavier Noria’s gist or my Pragmater gem for details. This is also a good reminder to keep Ruby Warnings enabled so you can stay on top of upcoming changes!

  • Previously, if supplying nil as an argument for a ** parameter (i.e. double splats), you’d get a TypeError because nil could not be implicitly converted into a hash. This is now fixed so double splat parameters support nil as an argument which is the equivalent of **{} as detailed in this bug. Despite nil equating to **{}, this does not allocate a new hash in memory, only that it’s ignored entirely. In related news, there is a feature request for improving *nil performance as well. For a deeper dive, check out my Method Parameters And Arguments article.

  • Related to the above, Jeremy Evans has made further memory performance improvements regarding the single splat (i.e. *), double splat (i.e. **), and argument forwarding (i.e. ...). For details, see his talk and/or corresponding slides.

  • it can now be used instead of hard to read numbered parameters. Example: "demo".tap { puts it }. There are some complications with this new behavior that were fixed and reverted in these issues related to obtaining parameters and local variables: 20955 and 20965. You’ll want to watch this space, especially if it is used for metaprogramming purposes, as you’l be in for surprising behavior.

  • The Array#each, Array#select, and Array#map methods have been rewritten in Ruby for improved performance. See Feature 20182 for details.

  • You’ll now get warnings when passing a block to a method which doesn’t use the block. Once again, don’t forget to keep your Ruby Warnings enabled so you can quickly detect and fix accordingly.

  • Array#fetch_values is a new method that is like Hash#fetch_values which, at the moment, isn’t entirely compatible with Array#values_at because you’ll get a TypeError. For example, if using [1, 2, 3, 4].values_at(0, 1..2), you’ll get [1, 2, 3] but if using [1, 2, 3, 4].fetch_values(0, 1..2), you’ll get a TypeError. This should be fixed in a future version.

  • Hash performance improvements have been applied to precompute embedded string literals. Additionally, Hash#inspect was updated to include spaces between the hash rockets and key/value pairs. Example: {"a"=>1, :b=>2} is now {"a" => 1, b: 2}. There is also Issue 20982 to update ENV#inspect to behave this way too.

  • Pathname gained .mktmpdir as an extra method while #mkpath and #rmtree answer the current pathname object. Due to this change, the Pathname#mkpath and Pathname#remove_tree Refinements methods will soon be removed to embrace native support. 🎉

  • Exception#set_backtrace now accepts an array of Thread::Backtrace::Location objects when using Kernel#fail, Thread#fail, or Fiber#fail. This allows you to rebuild a Backtrace instance with customized exception reporting like when using fail StandardError, "Danger!", caller_locations(3) to trim caller locations for reporting purposes. See Issue 13557 for further details.

  • Exception strings, like NameError for example, might trip up some specs in your test suite because an error message like `demo' is not defined is now 'demo' is not defined. This is subtle but notice how the former starts with a backtick versus the latter which starts with an apostrophe. Again, subtle, but worth being aware of.

  • Range#size will now fail with a TypeError when a range is not iterable. A good example is when using a beginless range: (..0).size. Prior to Ruby 3.4.0, this would answer Infinity but, in Ruby 3.4.0, this will now fail with a TypeError. This does change developer experience as noted in Issue 20980.

  • Range#step can now be used with any type, like dates/times, which makes this method more powerful. Previously, if you used (Time.now..).step(30).take 3, you’d end up with a can’t iterate from Time (TypeError) but with Ruby 3.4.0, you can use #step with dates, times, and other objects that are enumerable to get the following: [2025-01-08 06:00:09.718711 -0700, 2025-01-08 06:00:39.718711 -0700, 2025-01-08 06:01:09.718711 -0700].

YJIT

Maxime Chevalier-Boisvert has written how YJIT is Even Faster and More Memory-Efficient with 5 to 7% performance gains in this release. This includes new benchmarks, memory improvements, and enhancements to multiple objects and methods.

Prism

Prism is now the default parser so if you’ve been using RubyVM::AbstractSyntaxTree you’ll want to drop down to using RubyVM::InstructionSequence depending on your needs.

While use of the RubyVM isn’t fully supported by the Ruby core team for production use, there are times where it can be useful. For example, if you wanted to acquire the body of a function prior to Ruby 3.4.0, you could do this:

function = proc { Object.new }
ast = RubyVM::AbstractSyntaxTree.of function
ast.children.last.source

# "Object.new"

With Ruby 3.4.0, the above will yield the following error:

cannot get AST for ISEQ compiled by prism (RuntimeError)

The solution is to leverage Prism as follows:

function = proc { Object.new }
RubyVM::InstructionSequence.of(function).script_lines

# [
#   "function = proc { Object.new }\n",
#   "RubyVM::InstructionSequence.of(function).script_lines\n",
#   "\n",
#   ""
# ]

The above isn’t as elegant what we had before the parser was switched to Prism because you need to do extra work to extract Object.new out of the original source code but at least you can get close. Something that would be nice to see is this discussion between Samuel Williams and Benoit Daloze where you could have the following Object API:

function = proc { Object.new }
function.source.code  # "Object.new"

This doesn’t exist but would be nice to have especially for inspection, debugging, and Domain Specific Languages (DSLs).

Garbage Collection

Major Garbage Collection has been disabled entirely as detailed in Jean Boussier’s article. You can also configure the GC via GC.config.

Gems

  • JSON parsing and loading is now 1.7x faster as of Version 2.8.0. Details on how this came about can be found on Jean Boussier’s site: Optimizing Ruby’s JSON Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, and Part 7.

  • Repl Type Completer is bundled by default so you don’t have to install this gem separately now. You’ll still need to add it as a dependency to enhance your IRB session. This is already supported by Rubysmith if you are looking to build a new Ruby project with all bells and whistles included.

Namespaces

  • Namespaces are still planned for Ruby 4.0.0 so keep an eye on further developments. This includes ::Ruby being reserved as a top-level name (you’ll get warnings if attempting to use it).

Notes

You can find all of the above and more via the Ruby 3.4.0 News section of the Ruby Docs.