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_literalpragma 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
nilas an argument for a**parameter (i.e. double splats), you’d get aTypeErrorbecausenilcould not be implicitly converted into a hash. This is now fixed so double splat parameters supportnilas an argument which is the equivalent of**{}as detailed in this bug. Despitenilequating 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*nilperformance 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. -
itcan 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 ifitis used for metaprogramming purposes, as you’l be in for surprising behavior. -
The
Array#each,Array#select, andArray#mapmethods 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_valuesis a new method that is likeHash#fetch_valueswhich, at the moment, isn’t entirely compatible withArray#values_atbecause you’ll get aTypeError. 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 aTypeError. This should be fixed in a future version. -
Hashperformance improvements have been applied to precompute embedded string literals. Additionally,Hash#inspectwas 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 updateENV#inspectto behave this way too. -
Pathnamegained .mktmpdir as an extra method while #mkpath and #rmtree answer the current pathname object. Due to this change, thePathname#mkpathandPathname#remove_treeRefinements methods will soon be removed to embrace native support. 🎉 -
Exception#set_backtracenow accepts an array ofThread::Backtrace::Locationobjects when usingKernel#fail,Thread#fail, orFiber#fail. This allows you to rebuild aBacktraceinstance with customized exception reporting like when usingfail StandardError, "Danger!", caller_locations(3)to trim caller locations for reporting purposes. See Issue 13557 for further details. -
Exception strings, like
NameErrorfor example, might trip up some specs in your test suite because an error message like`demo' is not definedis 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#sizewill now fail with aTypeErrorwhen a range is not iterable. A good example is when using a beginless range:(..0).size. Prior to Ruby 3.4.0, this would answerInfinitybut, in Ruby 3.4.0, this will now fail with aTypeError. This does change developer experience as noted in Issue 20980. -
Range#stepcan 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#stepwith 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
::Rubybeing 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.
