What's new in Ruby 3.4
A new version of Ruby is released every year on Christmas day. The 3.4 release will be out next week, so I’ve been playing around with the release candidate. Here are some of the features I found interesting.
(As an aside, I just realized that this is the 10th year in a row that I’ve written one of these “what’s new in Ruby” recaps!) 🎉
🧱 it
- the default block parameter
Ruby introduced numbered block parameters
5 years ago in version 2.7.
This allowed us to use _1
, _2
etc
instead of explicit block parameters.
Now, a new variable called it
has been added
to refer to the default block parameter.
This can be handy when chaining multiple operations:
isbn.gsub("-", "")
.then { URI.parse("#{endpoint}?q=isbn:#{it}") }
.then { Net::HTTP.get(it) }
.then { JSON.parse(it) }
.then { extract_volume_info(it) }
def extract_volume_info(response)
return if response["totalItems"] == 0
response["items"]["volumeInfo"]
end
I like this as a shorthand for local method calls.
Previously, if you wanted shorthand syntax
for a method in local scope,
you’d write something like &method(:foo)
.
# old
list.map(&method(:transform))
# new
list.map { transform(it) }
With the &method
syntax,
the method name is a symbol,
making it harder for editors and IDEs
to identify it as a method reference
when using the “find references” feature.
With the new syntax,
it’s easier for tools to identify it as a method call.
The above code also allocates an unnecessary block object,
which is avoided in the it
version.
🥶 Chilled strings
Ruby 2.3 introduced the concept of frozen string literals,
with the frozen_string_literal: true
comment,
which would make any string literal immutable by default.
However, it hasn’t been possible
to make this the default behavior
due to compatibility concerns with many libraries.
Ruby 3.4 takes another step towards immutable strings
by introducing the concept of chilled strings.
Now files without the frozen_string_literal
comment
will treat string literals as “chilled”,
which means they can still be mutated,
but it will emit a warning on mutation.
You can use the -W:deprecated
flag
to see which strings are being mutated.
If you see a string being mutated,
make sure to explicitly mark it as mutable
by either calling dup
or using the unary +
operator.
Let’s take an example:
s = "foo"
s << "bar"
# foo.rb:2: warning: literal string will be frozen in the future
# (run with --debug-frozen-string-literal for more information)
Now if we run with --debug-frozen-string-literal
:
ruby -W:deprecated --debug-frozen-string-literal foo.rb
# foo.rb:2: warning: literal string will be frozen in the future
# foo.rb:1: info: the string was created here
To fix this, you can use
either s = "foo".dup
or use the unary +
operator
(s = +"foo"
).
# Works fine
s = "foo".dup
s << "bar" # no warning
# Also works fine
s = +"foo"
s << "bar" # no warning
(Note:
String.new
also creates mutable strings,
but as Ufuk points out,
it has some rough edges compared to the above options.)
🌈 Prism is the new default parser
Ruby 3.3 introduced a new parser for Ruby called Prism.
It is designed to be
more error tolerant, and easier to maintain.
With 3.4, it will ship as the default parser for Ruby,
replacing the lrama
parser generator,
which generates the parser
from the 16kLOC long
parse.y
file,
that is hard to maintain.
Prism is already the default for many other Ruby implementations (JRuby, TruffleRuby, Natalie, Opal) and community tools (Rubocop, RBI, Ruby LSP etc). Having it become the single source of language grammar makes it easier for tooling to keep up with changes in Ruby.
🗑️ Modular GC
Ruby’s garbage collection has been made more modular, so alternative garbage collectors can be dynamically loaded at runtime. As part of this, an experimental GC based on MMTk has been merged into Ruby.
This will allow more experimentation with modern high performance GCs, and make it easier to compare different GC implementations in production.
🚀 More YJIT improvements
Ever since YJIT became Ruby’s default JIT compiler in Ruby 3.1, we have seen incredible performance jumps every year. This year is no different. On YJIT benchmarks, YJIT is a whopping 92% faster compared to runnning Ruby without JIT.
Railsbench shows 95% speedup, but real world apps are usually more constrained by IO than benchmarks. However, many real world Rails apps have shown 15-20% speedups with YJIT in Ruby 3.3, so expect speedups in a similar ballpark. (Rails also enables YJIT by default now.)
An interesting development with YJIT
is that more of the core library methods
(such as Array#each
)
can now be rewritten in Ruby instead of C
and can take advantage of YJIT’s optimizations.
Aaron Patterson’s excellent article,
Ruby Outperforms C,
goes in depth into why this makes things faster.
🙈 Monkeypatch warnings
Ruby’s flexibility means that you can even monkeypatch core classes if you wanted to. However, many of these methods have special handling in the interpreter and JIT, and redefining them can negatively affect performance. With 3.4, redefining such methods will cause a performance warning. The feature request has a list of the methods that shouldn’t be redefined:
💎 Ruby toplevel module is reserved
The Ruby
namespace is now reserved,
which means that if you define a class or module called Ruby
,
you will see a warning.
With 3.5.0, a new Ruby
module will be introduced,
but for now there’s nothing in that namespace.
I expect we’ll eventually see things like Ruby::VERSION
exposed through the module
instead of top level constants like RUBY_VERSION
.
📚 Further reading
These “what’s new in Ruby” posts highlight changes that I personally found most interesting, and skip over features I might not use. If you’re looking for a more comprehensive look at the release, I highly recommend looking at the release announcement, and changelog. The Ruby References website is another fantastic resource if you want a deeper dive into all of the changes in this version.