Happy Holidays! ❄️☃️❄️
Ruby 4.0.0 is here along with several enhancements over last year’s Ruby 3.4.0 release. The following highlights the changes in this release. Definitely dig into the release notes for complete details.
Array
Array gains two new methods:
-
#find: Provides an efficient implementation overEnumerable#find(what was previously used). -
#rfind: Allows you to find the last element matching a specific condition which provides a performance improvement overArray#reverse_each.findby avoiding array allocation when messagingEnumerable#reverse_each. Example:
[1, 2, 3].rfind(&:even?) # 2
Binding
Methods such as local_variables, local_variable_get, and local_variable_set no longer handle numbered parameters.
Boxes
Boxes — which are a kind of Module — are a new experimental feature that allows you to work within isolated namespaces. They provide the following benefits:
-
Parallel testing and benchmarking of alternative implementations of your code.
-
Parallel blue-green deployment of your application.
-
A potential complement/alternative to Refinements.
For example, boxes give you a powerful way to benchmark two different versions of your code (which is something that was much harder to do previously):
#! /usr/bin/env ruby
# frozen_string_literal: true
# Save as `benchmark`, then `chmod 755 benchmark`, and run as `./benchmark`.
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "benchmark-ips"
end
One = Ruby::Box.new
One.require "one"
one = One::Demo.new
Two = Ruby::Box.new
Two.require "two"
two = Two::Demo.new
Benchmark.ips do |benchmark|
benchmark.config time: 5, warmup: 2
benchmark.report("One") { one.call }
benchmark.report("Two") { two.call }
benchmark.compare!
end
The only problem is that the above will cause the following error: NameError uninitialized constant Gem: :NameTuple. Definitely more kinks to be worked out with this experimental feature.
Additionally, the Object API is awkward in that you have to initialize a box instance. I wish you could use a block for quick use. Example:
Box = Ruby::Box do |instance|
instance.require "demo"
end
Even a keyword would be nice (granted, wishful thinking):
Box = box { |instance| instance.require "demo" }
See documentation for details.
CGI
If you were using CGI.parse to parse URIs, you’ll want to switch to Rack::Utils.parse_query since .parse has been removed.
Core
The following details the collection of changes to the core language.
The main site has been given a fresh coat of paint and is a nice upgrade compared to the previous design which was stuck in the latest 1990’s. Unfortunately, the landing page spends 50% of it’s time promoting Rails which is disappointing since Ruby is so much more than Rails.
The Ruby Object API Documentation has also been given a fresh coat of paint as well. Most significantly is better search support (including keyword shortcuts). 💡 For Alfred fans, definitely install the Pennyworth gem along with the Ruby Workflow to access Ruby resources with only a few keystrokes. 🎉
Object Allocation performance is now ~2.3 times faster for positional parameters and ~6 times faster for keyword parameters (with YJIT enabled).
A new top level Ruby module has been added (was first reserved in Ruby 3.4). This gives you access to several top level constants via Ruby.constants. Example:
%i[
PATCHLEVEL
Box
REVISION
COPYRIGHT
ENGINE
ENGINE_VERSION
VERSION
RELEASE_DATE
DESCRIPTION
PLATFORM
]
Performance warnings have been added for unnecessary implicit allocation of positional and keyword splats. This is also a good reminder to have Ruby Warnings enabled!
Use of *nil no longer calls nil.to_a, similar to how **nil does not call nil.to_hash. Important to be aware of especially in regards to Method Parameters And Arguments.
Using logical binary operators — ||, &&, and, and or — at the beginning of a line will be honored as the continuation of the previous line. Example:
puts "demo" if condition_a
&& condition_b
&& condition_c
Despite this addition, prefer using a single line instead of vertically stacking across multiple lines because this’ll help you identify a code smells and extract the logic to a separate method or improve the implementation to avoid using multiple conditionals altogether.
Fiber
Fiber#raise cause: was added and is similar to Kernel#fail cause:.
Fiber::Scheduler#fiber_interrupt has been added to allow a fiber to be interrupted with an exception. Initially, this is to deal with fibers waiting on a blocking I/O operation which is closed.
Kernel
Kernel#instance_variables_to_inspect — a new private method — was added as a hook to Kernel#inspect which means you can control what is exposed when inspecting an object. Example:
class Person
def initialize name, email, address
@name = name
@email = email
@address = address
end
private
def instance_variables_to_inspect = %i[@name @email]
end
Person.new("Jayne Cobb", "jayne@serenity.io", "Unknown").inspect
#<Person:0x000000014e1194b0 @name="Jayne Cobb", @email="jayne@serenity.io">
Person = Data.define :name, :email, :address do
private
def instance_variables_to_inspect = %i[@name @email]
end
Person[name: "Jayne Cobb", email: "jayne@serenity.io", address: "Unknown"].inspect
#<data Person name="Jayne Cobb", email="jayne@serenity.io", address="Unknown">
💡 To have this work for Class, Struct, and Data — along with the ability to filter and transform what is exposed — reach for the Inspectable gem.
Pathname
Pathname is now a core class instead of a default gem. For heavy Pathname fans, this means you can delete all occurrences of require "pathname" in your implementations for a nice reduction in lines of code used. This also means one less require for the Refinements gem if you need additional Pathname firepower.
💡 Ensure Ruby Warnings are enabled to quickly catch all occurrences.
Proc
Proc#parameters has been fixed to only answer [:opt] instead of [:opt, nil] when an anonymous optional parameter is used (i.e. it and _1). This was done to improve consistency when the anonymous parameter is required.
Ractor
Stability, performance, and usability enhancements have been applied to Ractor to where it’s getting close to becoming a full fledged feature instead of being an experimental one. In this release, the Ractor Object API has been redesigned to use Ractor::Port for communicating between ractors instead of using Ractor.yield and Ractor#take (which are now removed). Example:
port_a = Ractor::Port.new
port_b = Ractor::Port.new
Ractor.new port_a, port_b do |first, second|
first << 1
second << 2
first << 10
second << 20
end
2.times { puts port_a.receive } # 1, 10
2.times { puts port_b.receive } # 2, 20
Set
Set is now a core class instead of an autoloaded standard library class. Additionally — and in order to make Set consistent with other core collection classes (i.e. Array and Hash) — Set#inspect now answers a string which is compatible with Set[] which can be used with eval. Example:
# Before
#<Set: {1, 2, 3}>
# After
Set[1, 2, 3]
💡 This is a powerful use of Object Inspection.
String
String has been updated to Unicode 17.0.0 and Emoji 17.0.0 (this includes Regexp being updated as well).
The following String methods now accept character selectors. Example: #strip, #strip!, #lstrip, #lstrip!, #rstrip, and #rstrip!. Previously, these methods took no arguments. Here’s an example using #strip:
"agooda".strip "a" # "good"
"agooda".strip "ad" # "goo"
"01234good56789".strip "0-9" # "good"
"123abc57".strip("0-9", "^5", "^7") # "abc57"
Thread
You can supply a cause as done for Kernel#fail. Example: Thread#raise cause:.
ZJIT
A new Just In Time compiler, ZJIT has been introduced to eventually eclipse YJIT. You can play with ZJIT as follows:
ruby --zjit demo.rb
Worth experimenting with further but — keep in mind — this is not meant for production yet.
Conclusion
Upgrading from Ruby 3.4.0 to 4.0.0 should be fairly seamless but definitely take the time to refactor your code and make use of the new features.
