On making the Ruby Changes, and some consequences of this work

Every year since 2013, on December 25, a new Ruby version is released. (Before that, the version schedule was much unpredictable.)

Every year since 2018 (Ruby 2.6), I spend several weeks in December working on updating my Ruby Changes site to prepare an annotated changelog of the language, focused on changes in syntax, semantics, and core APIs. You can see the 2024 installment, dedicated to Ruby 3.4, here: 7k words, 30+ sections, and some ~60 hours of work.

I have already written a series of articles about how and why I am doing it in 2022 (1, 2, 3), along with some philosophical and personal implications of this work. Last year, I made it into an “advent” of following this work with explanations (which I myself called “a boring advent,” so I don’t think a lot of people have followed it).

So, for this year, I am just doing a short wrap-up about the new chunk of work done.

How it is done

From the very inception of the “Ruby Changes” idea, my goals were to cover all notable changes in every version (not only those I like or plan to use soon or those that are easy to describe), and to explain them the way that would be educative and instructive. This means linking to the relevant documentation, providing the reasoning behind the change and how it was decided to be, and showing the code examples—both illustrating the change’s utility and demonstrating edge case behavior.

The “all changes” goal has a big sidenote in the case of my work. The “Ruby release” that happens each year, is a release of the language and its particular, authoritative implementation: CRuby; thus, NEWS-file of each new version includes a lot of changes that are related to how CRuby works, like parsing, garbage collection and so on. My area of interests and area of expertise is about the language as a tool of thinking/writing; so I only mention changes of the “insides” only to the extent they are manifested in changes of the API of the core classes. Some years it makes “Ruby Changes” focus significantly diverge from NEWS-file’s focus (and the focus of many blog posts that rely on it). This year, which is marked by the introduction of the new default parser, and of pluggable GC concept, is not an exception.

So, every day of work during December, when I work on the changelog (it typically takes a few hours every day for a few weeks), I rebuild the most recent development version of Ruby and use it to write all examples to check how it all really works. I also read through every discussion on the official Ruby tracker (thankfully, the relevant issues are linked from each entry of the NEWS-file) and check the latest rendered docs at /master.

Every year, this activity, besides producing the changelog itself, allows spotting (and frequently fixing before the release) some deficiencies in documentation or even complicated edge cases on the new behavior. Here are some of this year’s examples.

Some consequences

A forewarning: by no means I want to imply that something was “broken” in the Ruby development process that I “fixed.” The small amount of work that I am doing during my Decembers is just a fragment of what the modern mature language maintainers are dealing with. There are a lot of people who maintain the language, its implementation, tools, and documentation all year round. I just do my small part—and they say, in the modern world, you should talk about your work. So, there’s that.

Documentation improvements

  • Documenting it (changelog entry)—the new anonymous parameter lacked the documentation, so I wrote some (and rephrased the numbered block parameters documentation appropriately).
  • Documenting **nil (changelog entry): While **nil construct (allowing to unpack it into empty keyword arguments) is a relatively minor change; trying to find the place in docs for it, I found it necessary to rephrase the whole section of the documentation about parameter unpacking (both positional and keyword ones)—hopefully, for the better.
  • Document backtrace-related changes (changelog entry): Structured caller/error backtrace values (in the form of Thread::Backtrace::Location) were introduced in Ruby 2.1, but only since 3.4 the structured backtrace can be set by a developer (when raising or post-processing the error). Integrating the concept properly with the documentation required some perspective changes in several places.

Behavior clarifications

it anonymous block parameter:

There are nuances related to the introspection of the parameters of the Proc (reported here):

# Old numbered arguments are introspected this way:
proc { _1 }.parameters    #=> [[:opt, :_1]]
lambda { _1 }.parameters  #=> [[:req, :_1]]

# But new `it` is reported differently,
# and inconsistently between proc and lambda:
proc { it }.parameters    #=> [[:opt, nil]]
lambda { it }.parameters  #=> [[:req]]

The local variables introspection for it-based blocks also behave differently from numbered parameters (reported here):

proc {
  _1
  p binding.local_variables
}.call
#=> [:_1]

proc {
  it
  p binding.local_variables
}.call
#=> [] -- `it` is not mentioned

Once I noticed the pair of problems, there was even an attempt to fix it. However, it turned out not that easy to do without introducing new inconsistencies/problems, so the “last-minute fix” was reverted. The desirable behavior will be discussed during the work on Ruby 3.5. Most probably, such inconsistencies wouldn’t affect the production code, but eventually, they are to be fixed for teaching/understanding the language value, if nothing else.

Array#fetch_values

The new method to fetch several values at once or raise/use defaults when they are absent (like Hash#fetch_value) was introduced. However, its accepted argument types are inconsistent with Array#values_at (the method that does the same, but without the control for absence):

[1, 2, 3, 4, 5].values_at(0, 3..4)    #=> [1, 4, 5]
[1, 2, 3, 4, 5].fetch_values(0, 3..4) # TypeError: in 'Array#fetch': no implicit conversion of Range into Integer

The discussion about proper behavior wasn’t finished in time for the 3.4.0 release (because it was reported late), so the decision will probably be made in the upcoming year.

Range#size has a new behavior to throw TypeError when the Range of a non-iterable type, but it has a small edge case:

(Time.now..Time.now+10).size
# 3.3: nil
# 3.4: can't iterate from Time (TypeError) -- new behavior

# But for beginless numeric ranges, there is a catch:
(..3).size
# 3.3: Infinity -- special handling, might be useful
# 3.4: can't iterate from NilClass (TypeError) -- is it worse?

Again, I hope to discuss this edge case during the work on Ruby 3.5.

A small funny thing that happened during producing the changelog examples is that I noticed that the (new) Ractor::[]/::[]= methods (Ractor-local storage) accepts both String and Symbol keys, like (existing) Thread#[]/#[]= but unlike (introduced in 3.2) Fiber::[]/::[]=, which only accepted Symbols. After reporting, the Fiber behavior was considered a bug and fixed.

A postcard from 🇺🇦

Please stop here for a moment. This is your regular mid-text reminder that I am a living person from Ukraine, with the Russian invasion still ongoing. Please read it.

One news item. In the first three days of the year, the russians killed three Ukrainian scientists: Ihor Zyma, a doctor of biological sciences and Olesya Sokur, a doctor of biological sciences, his wife, in Kyiv; Oleksiy Galyonka, a Candidate of Pedagogical Sciences (PhD) in Chernihiv.

One piece of context. Russians promote [Ukrainian] Christmas carols in Moskva as a “Russian cultural tradition, while at the same time banning Christmas caroling on the territories of Ukraine they’ve occupied.

One report. Stanislav Aseyev is a Ukrainian writer, journalist, veteran, and survivor of the Izolyatsia prison in Russia-occupied Donetsk, infamous for its torture of prisoners. He was the first Ukrainian journalist to see the Sednaya prison and death camp in Syria after the fall of Bashar al-Assad’s regime in 2024. Read his report.

One fundraiser. Please help this urgent fundraiser for a robotic ground systems unit.

My favorite overlooked feature this year

One of the reasons I started to do this work back in 2018 was an uneasy feeling of “(almost) everybody missing an important point.” Every year, many posts are published by various authors dedicated to explaining what’s new in Ruby. Almost every year, there is some change in syntax or API most of these posts overlook, probably considering it “some boring technical detail,” while for me, it feels like quality-of-life improvement stuff.

This year, such a feature was one developed by yours truly, so I apologize in advance for the shameless plug. It is a change in Range#step behavior with non-numeric ranges, which makes range iteration much more powerful.

As I explained in one of the recent articles, there was a long-standing historical inconsistency in Range#step iteration: with numbers, the argument was treated as “an amount to add on each step,” while for all other types, it meant “number of times to make a step forward with #succ”:

(1..3).step(0.5).to_a # uses +
#=> [1.0, 1.5, 2.0, 2.5, 3.0]

require 'active_support/all'
(Time.now..).step(30.minutes).take(2) # tries to use #succ
# TypeError (can't iterate from Time)

So the only usage of #step with non-numeric types was “several steps at a time”:

('a'..'e').step(2).to_a #=> ["a", "c", "e"]

Since Ruby 3.4, ranges of any type can use #step in a way that feels natural:

(Time.now..).step(30.minutes).take(3)
#=> [2025-01-04 21:39:44.369461267 +0200, 2025-01-04 22:09:44.369461267 +0200, 2025-01-04 22:39:44.369461267 +0200]

require 'unitwise'
(Unitwise(1, :km)..Unitwise(2, :km)).step(Unitwise(500, :m)).to_a
# => [#<Unitwise::Measurement value=1 unit=km>, #<Unitwise::Measurement value=1.5 unit=km>, #<Unitwise::Measurement value=2 unit=km>]

require 'matrix'
(Vector[1, 1]..).step(Vector[1, 1]).take(3)
#=> [Vector[1, 1], Vector[2, 2], Vector[3, 3]]

I expect time iteration to be the most widespread usage of the new feature, but I like it for introducing more power/consistency into the language.

Future possibilities

For a couple of years now, I promised myself to start maintaining the changelog “alive,” having the master section during the year (the same way Ruby documentation has the master branch version always rendered).

I hope this approach might allow to make people acquainted with what’s coming; it will also allow my “side effect” activities of changelog preparation to be done in a timely manner—which is especially important for confirming the edge cases like some with it and Array#fetch_values discussed above. Also, it would allow me to notice some other necessary documentation improvements. I frequently notice some things worth improving in existing docs (the methods neighboring or similar to the changed ones) but usually put them in my “long TODO” list… To never act upon, once the rush of finalizing this year’s release is over.

Maybe this year will be the one when I’ll finally act on this intention. Maybe not. We’ll see.


Thank you for reading. Please support Ukraine with your donations and lobbying for military and humanitarian help. Here, you’ll find a comprehensive information source and many links to state and private funds accepting donations.

If you don’t have time to process it all, donating to Come Back Alive foundation is always a good choice.