Ruby 3.3 highlights
It’s that time of the year again! Or at least it was a month ago. This Christmas I found myself spending more time out hiking and watching TV shows that had been on my list1 for far too long.
Before I knew it, I was at the end of the holidays and this post would have to sit half-written in my drafts while I got on with January. I’m happy to report that January is done and we can all start enjoying longer days and warmer weather2.
So here they are—a month later than planned, but hopefully just as useful—my Ruby 3.3 highlights.
IRB improvements
IRB is Ruby’s built-in REPL. For a long time, it trailed behind third-party alternatives like Pry when it came to features, but in recent versions that’s changed.
This year, we’ve got another batch of improvements that will make building and debugging Ruby apps that bit nicer.
Debugger integration
Since Ruby 2.5, it’s been possible to use binding.irb
as a breakpoint in your code that stops execution and drops you into an IRB session. In Ruby 3.3, there’s an added bonus: you can type debug
in that session and gain access to all the functionality of the debug
gem.
For example, in the Greeter
class below, we call binding.irb
just before the greeting is printed:
# greeter.rb
class Greeter
def initialize(recipient)
@recipient = recipient
end
def greet
binding.irb
puts "Hello #{@recipient}!"
end
end
greeter = Greeter.new("Ruby 3.3 fans")
greeter.greet
If we run that program, we’ll be dropped into an IRB REPL:
# ruby greeter.rb
From: greeter.rb @ line 7 :
2: def initialize(recipient)
3: @recipient = recipient
4: end
5:
6: def greet
=> 7: binding.irb # Execution pauses here
8: puts "Hello #{@recipient}!"
9: end
10: end
11:
12: greeter = Greeter.new("Ruby 3.3 fans")
irb():001> debug # We start the debugger
irb:rdbg():002> info
%self = #<Greeter:0x00007f7b737fc808 @recipient="Ruby 3.3 fans">
_ = nil
@recipient = "Ruby 3.3 fans" # We can see the values available in the current scope
Having that integration baked into IRB gives us a much more powerful REPL out-of-the-box and saves having to add an extra dependency to smaller projects.
Pager support for long output
Previously, if a command like show_source
produced a large amount of output, that output would fly past and you’d only see the last part of it. In Ruby 3.3, you’re able to navigate through that output page-by-page if it’s larger than the size of your terminal:
irb():001> require "net/ssh"
irb():002> show_source Net::SSH
From: /home/sinjo/projects/blog/.bundle/ruby/3.3.0/gems/net-ssh-7.2.1/lib/net/ssh/config.rb:2
module SSH
# The Net::SSH::Config class is used to parse OpenSSH configuration files,
# and translates that syntax into the configuration syntax that Net::SSH
# ...
: # Pager prompt
Better autocompletion (experimental)
The existing—and still default—implementation of autocompletion uses regular expressions to find potential classes and methods that match what you’re typing. In Ruby 3.3, you can install the experimental IRB::TypeCompletor
gem to enable completion based on type analysis.
$ gem install repl_type_completor
In an IRB session using the default regex-based completor, we get suggestions for the first level of method call on an object, but not on chained method calls.
# Single level of method calls - we get suggestions
$ irb
irb(main):001> ["jorts", "4", "eva!"].j
"eva!"].join(" ")
irb(main):001> ["jorts", "4", "eva!"].join(" ")
=> "jorts 4 eva!"
# Chained method calls - we don't get suggestions
$ irb
irb(main):001> ["jorts", "4", "eva!"].join(" ").u
With the new type-based completor, we keep getting suggestions after that first method call, provided the type information is available:
$ irb --type-completor
irb(main):001> ["jorts", "4", "eva"].join(" ").up
"eva"].join(" ").upcase
"eva"].join(" ").upcase!
"eva"].join(" ").upto
irb(main):001> ["jorts", "4", "eva"].join(" ").upcase
=> "JORTS 4 EVA!"
For more details, including how to set up IRB::TypeCompletor
as the default completor, check out the docs.
I think it’s awesome how much the Ruby core team still cares about developer experience. The language is coming up on three decades old and it keeps getting better.
A new Ruby parser: Prism
You might be wondering why I’m including something as behind-the-scenes as a parser implementation in these release highlights. While it’s not something 99% of the Ruby community will even be aware of, I do think it will have a positive impact for most of those users. This is the kind of work often goes unsung, but it’s what drives a language ecosystem forward.
So it turns out Ruby has a long and somewhat unfortunate history with parsers.
The parser that came with the standard version of Ruby (CRuby) was deeply intertwined with the internals of the implementation. This made it a non-starter for writing tools that need to parse Ruby code (like RuboCop, rubyfmt, and ruby-lsp) as well as alternative language implementations (like JRuby and TruffleRuby) as they need to be able to run outside of the context of the CRuby runtime.
Because of that, we ended up with multiple independent reimplementations of the parser—as many as 12, going by the blog post announcing the work on Prism. What this meant in practice was endless subtle bugs, incompatibilities, and ultimately frustration for library maintainers and end users. Hopefully those days are coming to a close.
Now that we have an official, portable parser for the Ruby language, we should start to see more consistent behaviour across those tools. As a bonus, this will lower the barrier to entry for anyone who wants to write new tools that work with the Ruby programming language, so we may be in for a small tooling renaissance.
I do recommend giving the whole announcement post a read if you have the time. It goes into way more depth about the problems being solved and the decisions the team made along the way.
That’s all I have this time around, but if you’re feeling curious there’s plenty more cool stuff that got into Ruby 3.3.
See you in a year for 3.4! ✌🏻💖
-
After spending much of 2023 saying I should do more to practise my Spanish, I’ve picked La Casa de Papel (AKA Money Heist) back up. Since I was only five episodes in, I decided to go back to the start and remind myself of the characters. Hopefully I won’t get distracted this time! ↩
-
Unless you live in the Southern Hemishpere, in which case: sucks to suck I guess. ↩