Ruby 3.0
- Released at: Dec 25, 2020 (NEWS.md file)
- Status (as of Dec 25, 2023): 3.0.6 is security maintenance (will EOL soon!)
- This document first published: Dec 25, 2020
- Last change to this document: Dec 25, 2023
Highlights
Ruby 3.0 is a major language release. The core team worked hard to preserve backward compatibility while delivering some huge and exciting new features.
- Full separation of keyword arguments
- Ractors: Thread-alike object implementing the actor model, and finally lifting the GVL (Global Virtual machine Lock) and enabling true concurrency
- Non-blocking IO with Fibers
- Type declarations (in separate files)
- Pattern matching:
- No longer experimental
- Two flavors for one-line pattern matching:
=>
(aka rightward assignment) andin
(aka boolean check) - Find patterns
- “Endless” methods
- GC auto-compaction
Language changes
Keyword arguments are now fully separated from positional arguments
The separation which started in 2.7 with deprecations, is now fully finished. It means keyword arguments are not a “syntax sugar” on top of hashes, and they never converted into each other implicitly:
- Discussion: Feature #14183
- Code:
def old_style(name, options = {}) end def new_style(name, **options) end new_style('John', {age: 10}) # Ruby 2.6: works # Ruby 2.7: warns: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call # Ruby 3.0: ArgumentError (wrong number of arguments (given 2, expected 1)) new_style('John', age: 10) # => works h = {age: 10} new_style('John', **h) # => works, ** is mandatory # The last hash argument still allowed to be passed without {}: old_style('John', age: 10) # => works
- Notes: There is a big and detailed explanation of the separation reasons, logic, and edge cases on Ruby site, written at the dawn of 2.7, so we will not go into more details here.
Procs with “rest” arguments and keywords: change of autosplatting behavior
Just a leftover from the separation of keyword arguments.
- Discussion: Feature #16166
- Code:
block = proc { |*args, **kwargs| puts "args=#{args}, kwargs=#{kwargs}"} block.call(1, 2, a: true) # Ruby 2.7: args=[1, 2], kwargs={:a=>true} -- as expected # Ruby 3.0: args=[1, 2], kwargs={:a=>true} -- same block.call(1, 2, {a: true}) # Ruby 2.7: # warning: Using the last argument as keyword parameters is deprecated # args=[1, 2], kwargs={:a=>true} -- but extracted to keyword args nevertheless # Ruby 3.0: # args=[1, 2, {:a=>true}], kwargs={} -- no attempt to extract hash into keywords, and no error/warning
- Follow-up: In Ruby 3.2, one more proc argument splatting behavior was improved.
Arguments forwarding (...
) supports leading arguments
- Reason: Argument forwarding, when introduced in 2.7, was able to forward only all-or-nothing. It turned out to be not enough. One of the important usages for leading arguments are cases like
method_missing
and other DSL-defined methods that need to pass to the nested method:some_symbol
+ all of the original arguments. - Discussion: Feature #16378
- Documentation:
doc/syntax/methods.rdoc
(the link is to amaster
version, docs were merged post 3.0 release) - Code:
def request(method, url, headers: {}) puts "#{method.upcase} #{url} (headers=#{headers})" end def get(...) request(:get, ...) end get('https://example.com', headers: {content_type: 'json'}) # GET https://example.com (headers={:content_type=>"json"}) # Leading arguments may be present both in the call and in the definition: def logged_get(message, ...) puts message get(...) end logged_get('Logging', 'https://example.com', headers: {content_type: 'json'})
- Notes:
- The adjustment was considered important enough to be backported to 2.7 branch;
- “all arguments splat”
...
should be the last statement in the argument list (both on a declaration and a call) - on a method declaration, arguments before
...
can only be positional (not keyword) arguments, and can’t have default values (it would beSyntaxError
); - on a method call, arguments passed before
...
can’t be keyword arguments (it would beSyntaxError
); - make sure to check your punctuation thoroughly, because
anything ...
is a syntax for endless range, those constructs are valid syntax, but would do not what is expected:def delegates(...) # call without "()" -- actually parsed as (p()...) p ... # Prints nothing, but warns: warning: ... at EOL, should be parenthesized? # "," accidentally missing after 1, there is just one argument: 1... p(1 ...) # Prints "1..." p(1, ...) # Prints, as expected: # 1 # 5 end delegates(5)
- Follow-ups:
- In Ruby 3.1, a separate anonymous block argument (bare
&
) forwarding was added; - In Ruby 3.2, separate positional and keyword (bare
*
and**
) forwarding were added.
- In Ruby 3.1, a separate anonymous block argument (bare
“Endless” method definition
Methods of exactly one statement now can be defined with syntax def method() = statement
. (The syntax doesn’t need end
, hence the “endless definition” moniker.)
- Reason: Ruby’s
end
s (unlike C-like languages’{}
) are generally OK for Rubyists, but make small utility methods look more heavy than they should. For small utility methods with body containing just one short statement like:def available? !@internal.empty? end
…the “proper” definition might look so heavy that one will decide against it to leave the class more readable (and instead make class’ clients to just do
obj.internal.empty?
themselves, making it less semantical). For such cases, a shortcut one-line definition may change the perceiving of utility method creation:def available? = !@internal.any? def finished? = available? && @internal.all?(&:finished?) def clear = @internal.clear
- Discussion: Feature #16746
- Documentation:
doc/syntax/methods.rdoc
(the link is to amaster
version, docs were merged post 3.0 release) - Code:
def dbg = puts("DBG: #{caller.first}") dbg # Prints: DBG: test.rb:3:in `<main>' # The method definition supports all kinds of arguments: def dbg_args(a, b=1, c:, d: 6, &block) = puts("Args passed: #{[a, b, c, d, block.call]}") dbg_args(0, c: 5) { 7 } # Prints: Args passed: [0, 1, 5, 6, 7] # For argument definition, () is mandatory def square x = x**2 # syntax error, unexpected end-of-input -- because Ruby treats it as # def square(x = x**2) # ...e.g. an argument with default value, referencing itself, and no method body # This works def square(x) = x**2 square(100) # => 10000 # To avoid confusion, defining method names like #foo= is prohibited class A # SyntaxError "setter method cannot be defined in an endless method definition": def attr=(val) = @attr = val # Other suffixes are OK: def attr?() = !!@attr def attr!() = @attr = true end # funnily enough, operator methods are OK, including #== class A def ==(other) = true end p A.new == 5 # => true # any singular expression can be method body # This works: def read(name) = File.read(name) .split("\n") .map(&:strip) .reject(&:empty?) .uniq .sort # Or even this, though, what's the point?.. def weird(name) = begin data = File.read(name) process(data) true rescue false end # inside method body, method calls without parentheses cause a syntax error: def foo() = puts "bar" # ^ syntax error, unexpected string literal, expecting `do' or '{' or '(' # This is due to parsing ambiguity and is aligned with some other places, like x = 1 + sin y # ^ syntax error, unexpected tIDENTIFIER, expecting keyword_do or '{' or '('
- Notes:
- The initial proposal seems to be a good-natured April Fool’s joke, then everybody suddenly liked it, and, with a slight change of syntax, it was accepted;
- Feature is marked as EXPERIMENTAL, but it does NOT produce a warning, it is deliberate, see discussion in Misc #17399.
- Follow-up: In Ruby 3.1, the requirement to wrap method calls in parenthesis inside endless method body was removed.
Pattern matching
Pattern matching, introduced in 2.7, is no longer experimental. Discussion: Feature #17260.
One-line pattern matching with =>
- Reason: This is an interesting one. Two facts were discussed between 2.7 and 3.0: the fact that in most of the other languages one-line pattern matching has a different order (
<pattern> <operator> <data>
) than introduced in Ruby 2.7 (<data> in <pattern>
); and the idea of “rightward assignment operator”=>
for more natural chaining. And then, at some point, ideas converged most fruitfully. - Discussion: Feature #17260 (main pattern matching tracking ticket), Feature #16670 (reverse order), Feature #15921 (standalone rightward assignment operator), Feature #15799 (abandoned “pipeline operator” idea, in discussion of which “rightward assignment” was born)
- Documentation:
doc/syntax/pattern_matching.rdoc
- Code:
# match and unpack: {db: {user: 'John', role: 'admin'}} => {db: {user:, role:}} p [user, role] # => ["John", "admin"] # pattern-matching as a rightward assignment for long experessions: File.read('test.txt') .split("\n") .map(&:strip) .reject(&:empty?) .first(10) => lines p lines # first 10 non-empty lines of the file # unpacking+assignment is extremely powerful: (1..10).to_a.shuffle => [*before, (2..4) => threshold, *after] # ...in input sequence, find first entry in range 2..4, put it into `threshold`, # and split parts of the sequence before/after it p [before, threshold, after] # your results might be different due to shuffle :) # => [[7, 5, 8], 3, [1, 10, 6, 9, 4, 2]] # The things can get really out of hand quickly: Time.now.hour => ..9 | 18.. => non_working_hour
- Notes:
- Feature is marked as EXPERIMENTAL, will warn so on an attempt of usage, and may change in the future;
- But simple assignment usage (
data => variable
) is not considered experimental and is here to stay; - One quirk that might be non-obvious: pattern matching can desconstruct-assign only to local variables, so when using
=>
as an assignment operator, you will see those are syntax errors:some_statement => @x some_statement => obj.attr # meaning to call `obj.attr=` some_statement => $y # ...though maybe don't use global variables :)
- Follow-ups:
- As of 3.1, the feature is no longer experimental;
- In 3.1, the parenthesis around the pattern became optional.
in
as a true
/false
check
After the change described above, in
was reintroduced to return true
/false
(whether the pattern matches) instead of raising NoMatchingPatternError
.
- Reason: The new meaning allows pattern matching to be more tightly integrated with other constructs in the control flow, like iteration and regular conditions.
- Discussion: Feature #17371
- Documentation:
doc/syntax/pattern_matching.rdoc
- Code:
user = {role: 'admin', login: 'matz'} if user in {role: 'admin', login:} puts "Granting admin scope: #{login}" end # otherwise just proceed with regular scope, no need to raise users = [ {name: 'John', role: 'user'}, {name: 'Jane', registered_at: Time.new(2017, 5, 8) }, {name: 'Barb', role: 'admin'}, {name: 'Dave', role: 'user'} ] old_users_range = Time.new(2016)..Time.new(2019) # Choose for some notification only admins and old users users.select { |u| u in {role: 'admin'} | {registered_at: ^old_users_range} } #=> [{:name=>"Jane", :registered_at=>2017-05-08 00:00:00 +0300}, {:name=>"Barb", :role=>"admin"}]
- Notes:
- Feature is marked as EXPERIMENTAL, will warn so on an attempt of usage, and may change in the future.
- Follow-ups:
- As of 3.1, the feature is no longer experimental;
- In 3.1, the parenthesis around the pattern became optional.
Find pattern
Pattern matching now supports “find patterns”, with several splats in them.
- Discussion: Feature #16828
- Documentation:
doc/syntax/pattern_matching.rdoc
- Code:
users = [ {name: 'John', role: 'user'}, {name: 'Jane', role: 'manager'}, {name: 'Barb', role: 'admin'}, {name: 'Dave', role: 'manager'} ] # Now, how do you find admin with just pattern matching?.. # Ruby 3.0: case users in [*, {name:, role: 'admin'}, *] # Note the pattern: find something in the middle, with unknown number of items before/after puts "Admin: #{name}" end # => Admin: Barb # Without any limitations to choose the value, the first splat is non-greedy: case users in [*before, user, *after] puts "Before match: #{before}" puts "Match: #{user}" puts "After match: #{after}" end # Before match: [] # Match: {:name=>"John", :role=>"user"} # After match: [{:name=>"Jane", :role=>"manager"}, {:name=>"Barb", :role=>"admin"}, {:name=>"Dave", :role=>"manager"}] # Guard clause does not considered when choosing where to splat: case users in [*, user, *] if user[:role] == 'admin' puts "User: #{user}" end # => NoMatchingPatternError -- it first put John into `user`, # and only then checked the guard clause, which is not matching # If the "find pattern" is used (there is more than one splat in the pattern), # there should be exactly TWO of them, and they may ONLY be the very first and # the very last element: case users in [first_user, *, {name:, role: 'admin'}, *] # ^ syntax error, unexpected * puts "Admin: #{name}" end # Singular splat is still allowe in any place: case users in [{name: first_user_name}, *, {name: last_user_name}] puts "First user: #{first_user_name}, last user: #{last_user_name}" end # => First user: John, last user: Dave
- Notes:
- Feature is marked as EXPERIMENTAL, will warn so on an attempt of usage, and may change in the future.
- Follow-ups: Feature considered not experimental since 3.2
Changes in class variable behavior
When class @@variable
is overwritten by the parent of the class, or by the module included, the error is raised. In addition, top-level class variable access also raises an error.
- Reason: Class variables, with their “non-intuitive” access rules, are frequently considered a bad practice. They still can be very useful for tracking something across class hierarchy. But the fact that the entire hierarchy shares the same variable may lead to hard-to-debug bugs, so it was fixed to raise on the usage that seems unintentional.
- Discussion: Bug #14541
- Documentation: —
- Code:
# Intended usage: parent defines the variable available for all children class Good @@registry = [] # assume it is meant to store all the children def self.registry @@registry end end class GoodChild < Good def self.register! @@registry << self @@registry = @@registry.sort # reassigning the value -- but it is still the PARENT's variable end end GoodChild.register! p Good.registry # => [GoodChild] # Unintended usage: the variable is defined in the child, but then the parent changes it class Bad def self.corrupt_registry! @@registry = [] end end class BadChild < Bad @@registry = {} # This is some variable which meant to belong to THIS class def self.registry @@registry end end Bad.corrupt_registry! # Probably unexpected for BadChild's author, its ancestor have changed the variable BadChild.registry # On the next attempt to _access_ the variable the error will be raised # 2.7: => [] # 3.0: RuntimeError (class variable @@registry of BadChild is overtaken by Bad) # The same error is raised if the included module suddenly changes class module OtherRegistry @@registry = {} end Good.include OtherRegistry Good.registry # On the next attempt to _access_ the variable the error will be raised # 2.7: => {} # 3.0: RuntimeError (class variable @@registry of Good is overtaken by OtherRegistry)
Other changes
- Global variables that lost their special meaning, just a regular globals now:
$SAFE
(the global setting related to tainting, deprecated in 2.7) – Feature #16131$KCODE
(legacy encoding setting) – Feature #17136
yield
in a singleton class definitions, which was deprecated in 2.7 is nowSyntaxError
– Feature #15575- Assigning to a numbered parameter (introduced in 2.7) is now a
SyntaxError
instead of a warning.
Types
The discussion of possible solutions for static or gradual typing and possible syntax of type declarations in Ruby code had been open for years. At 3.0, Ruby’s core team made their mind towards type declaration in separate files and separate tools to check types. So, as of 3.0:
- type declarations for Ruby code should be defined in separate files with extension
*.rbs
- the syntax for type declarations is like following (small example):
class Dog attr_reader name: String def initialize: (name: String) -> void def bark: (at: Person | Dog | nil) -> String end
- type declaration for the core classes and the standard library is shipped with the language;
- rbs library is shipped with Ruby, providing tools for checking actual types in code against the declarations; and for auto-detecting (to some extent) the actual types of yet-untyped code;
- TypeProf is another tool (“Type Profiler”) shipping with Ruby 3.0 as a bundled gem, allowing to auto-detect the actual types of the Ruby by “abstract interpretation” (running through code without actually executing it);
For a deeper understanding, please check those tools’ documentation; also this article by Vladimir Dementiev of Evil Martians takes a detailed look into tools and concepts.
Core classes and modules
Object#clone(freeze: true)
freeze:
argument now works both ways (previously only freeze: false
had an effect).
- Reason: Mostly for consistency.
Object#clone(freeze: false)
was introduced in Ruby 2.4 as an only way of producing unfrozen object;clone(freeze: true)
is basically an equivalent ofclone
+freeze
. - Discussion: Feature #16175
- Documentation:
Kernel#clone
- Code:
o = Object.new o.clone(freeze: true).frozen? # => false in Ruby 2.7 # => true in Ruby 3.0 o = Object.new.freeze o.clone(freeze: false).frozen? # => false in Ruby 2.7 and 3.0
Object#clone
passes freeze:
argument to #initialize_clone
Specialized constructor #initialize_clone
, which is called when object is cloned, now receives freeze:
argument if it was passed to #clone
.
- Reason: For composite objects, it is hard to address freezing/unfreezing of nested data in
#initialize_clone
without this argument. - Discussion: Bug #14266
- Documentation:
Kernel#clone
- Code:
require 'set' set = Set[1, 2, 3].freeze set.frozen? # => true set.instance_variable_get('@hash').frozen? # => true, as expected unfrozen = set.clone(freeze: false) unfrozen.frozen? # => false, as expected unfrozen.instance_variable_get('@hash').frozen? # 2.7: => true, still # 3.0: => false, as it should be -- becase Set have redefined #initialize_clone unfrozen << 4 # 2.7: FrozenError (can't modify frozen Hash: {1=>true, 2=>true, 3=>true}) # 3.0: => #<Set: {1, 2, 3, 4}>
- Notes: A lot of attention to proper object freezing in Ruby 3.0 is due to the introduction of Ractors, which made the important distinction if the object is truly frozen (and therefore safe to share between parallel ractors).
Kernel#eval
changed processing of __FILE__
and __LINE__
Now, when the second argument (binding
) is passed to eval
, __FILE__
in evaluated code is (eval)
, and __LINE__
starts from 1
(just like without binding
). Before 2.7 it was evaluated in context of binding
(e.g. returned file from where binding
came from), on 2.7 the warning was printed; now the behavior is considered final.
- Reason: Binding might be passed to
eval
in order to provide the access to some context necessary for evaluation (this technique, for example, frequently used in templating engines); but it had an unintended consequence of making__FILE__
and__LINE__
to point not to actually evaluated code, which can be misleading, for example, on errors processing. - Discussion: Bug #4352
- Affected methods:
Kernel#eval
,Binding#eval
- Code:
# file a.rb class A def get_binding binding end end # file b.rb require_relative 'a' eval('p [__FILE__, __LINE__]') # without binding eval('p [__FILE__, __LINE__]', A.new.get_binding) # with binding from another file # Ruby 2.6: # ["(eval)", 1] # ["a.rb", 3] # Ruby 2.7: # ["(eval)", 1] # ["a.rb", 3] # warning: __FILE__ in eval may not return location in binding; use Binding#source_location instead # warning: __LINE__ in eval may not return location in binding; use Binding#source_location instead # Ruby 3.0: # ["(eval)", 1] # ["(eval)", 1]
Regexp
and Range
objects are frozen
- Reason: The change is related to Ractor introduction (see below): it makes a difference whether an object is frozen when sharing it between ractors. As both ranges and regexps have immutable core data, it was considered that making them frozen is the right thing to do.
- Discussion: Feature #8948, Feature #16377, Feature #15504
- Code:
/foo/.frozen? # => true (42...).frozen? # => true # Regexps are frozen even when constructed with dynamic interpolation /.#{rand(10)}/.frozen? # => true # ...but not when they are constructed with the constructor Regexp.new('foo').frozen? # => false # ...but ranges are always frozen Range.new('a', 'b').frozen? # => true # Regularly, as the data can't be changed anyways, the frozenness wouldn't affect your code. # It might, though, if the code does something smart like: regexp = /^\w+\s*\w*$/ regexp.instance_variable_set('@context', :name) # 2.7: ok # 3.0: FrozenError (can't modify frozen Regexp: /^\w+\s*\w*$/) # ...or RANGE = Time.new(2020, 3, 1)..Time.new(2020, 9, 1) def RANGE.to_s self.begin.strftime('%Y, %b %d') + ' - ' + self.end.strftime('%b %d') end # 2.7: OK # 3.0: FrozenError (can't modify frozen object: 2020-03-01 00:00:00 +0200..2020-09-01 00:00:00 +0300) # Note also, that range freezing is not "deep": string_range = 'a'..'z' string_range.end.upcase! string_range # => 'a'..'Z' # clone(freeze: false) still allows to unfreeze both: unfrozen = RANGE.clone(freeze: false) def unfrozen.to_s self.begin.strftime('%Y, %b %d') + ' - ' + self.end.strftime('%b %d') end puts unfrozen # Prints: "2020, Mar 01 - Sep 01"
- Notes: The fact that dynamically created ranges are frozen and regexps are not, is explained this way: Ideally, both should be frozen, yet there are some gems (and some core Ruby tests) that use dynamic singleton method definitions on regexps, and it was decided that incompatibility should be avoided.
Module
#include
and #prepend
now affects modules including the receiver
If class C
includes module M
, and after that we did M.include M1
, before Ruby 3.0, M1
would not have been included into C
; and now it is.
- Reason: The behavior was long-standing and led to a lot of confusion (especially considering that including something in the class did affected its descendants).
- Discussion: Feature #9573
- Documentation: —
- Code:
module MyEnumerableExtension def each2(&block) each_slice(2, &block) end end Enumerable.include MyEnumerableExtension (1..8).each2.to_a # Ruby 2.7: NoMethodError (undefined method `each2' for 1..8:Range) -- even though Range includes Enumerable # Ruby 3.0: [[1, 2], [3, 4], [5, 6], [7, 8]]
- Follow-ups: In Ruby 3.1,
#prepend
behavior got adjusted again to be more predictable.
Improved method visibility declaration
private attr_reader :a, :b, :c
and private alias_method :foo, :bar
now work exactly like one might expect.
- Changed behaviors:
Module#public
,#private
,#protected
can now accept an array. While it is not necessary when a literal list of symbols is passed, it is more convenient when the method names are calculated by some DSL method.Module#attr_accessor
,#attr_reader
,#attr_writer
now return arrays of symbols with new methods names defined.Module#alias_method
now returns a name of the method defined.
- Documentation:
Module#private
,Module#public
,Module#protected
,Module#public_class_method
,Module#private_class_method
are accepting arrays;Module#attr
,Module#attr
,Module#attr
,Module#attr_accessor
are returning arrays of created method names;Module#alias_method
returns the name of created method;
- Discussion: Feature #17314
- Code:
class A def foo end def bar end # new behavior of private private %i[foo bar] # Ruby 2.7: [:foo, :bar] is not a symbol nor a string # Ruby 3.0: works # old behavior still works private :foo, :bar # new behavior of attr_XX p(attr_reader :a, :b) # Ruby 2.7: => nil # Ruby 3.0: => [:a, :b] # new behavior of alias_method p(alias_method :baz, :foo) # Ruby 2.7: => A # Ruby 3.0: => :bar # The consequence is this is now possible: private attr_reader :first, :second # attr_reader() returns array [:first, :second], which is then passed to private() private alias_method :third, :second # alias_method() returns :third, which is then passed to private() end
- Note: Unlike
alias_method
,alias
is not a method but a language construct, and its behavior haven’t changed (and it can’t be used in an expression context):class A def foo end private alias bar foo # ^ syntax error, unexpected `alias' end
- Follow-up: In Ruby 3.1,
#private
,#protected
,#public
, and#module_function
started to return their arguments for better macros chainability.
Warning#warn
: category:
keyword argument.
Ruby 2.7 introduced warning categories, and allowed to selectively suppress them with Warning[]=
method. Since Ruby 3.0, user code also may specify categories for its warnings, and intercept them by redefining Warning.warn
.
- Discussion: Feature #17122
- Documentation:
Warning#warn
,Kernel#warn
- Code:
# Using from user code: Warning[:deprecated] = true warn('my warning', category: :deprecated) # "my warning" Warning[:deprecated] = false warn('my warning', category: :deprecated) # ...nothing, obeys "don't show deprecated" setting # If the category is not supported: warn('my warning', category: :custom) # ArgumentError (invalid warning category used: custom) # Intercepting: module Warning def self.warn(msg, category: nil) puts "Received message #{msg.strip} with category=#{category}" end end Warning[:deprecated] = true lambda(&:foo?) # Received message 'warn.rb:23: warning: lambda without a literal block is deprecated; use the proc without lambda instead' with category=deprecated eval('[1, 2, 3] => [x, *]') # we use eval, otherwise the warning was raised on PARSING stage, before any redefinitions # Received message '(eval):1: warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!' with category=experimental
Exception output order is changed – again
The “reverse” order of the backtrace (call stack printed starting from the outermost layer, and the innermost is at the bottom), which was introduced in 2.5 as “experimental”, tweaked to be active only for STDERR
on 2.6, now it is reverted to the old (pre-2.5) behavior.
- Reason: “From the outer to the inner” order is the default for some other languages (like Python), but it seems most of the rubyists never got used to it.
- Discussion: Feature #8661 (the whole history of the “backtrace reversing” initiative, with all the turns)
- Code:
def inner raise 'test' end def outer inner end outer
This prints on Ruby 2.5-2.7:
Traceback (most recent call last): 2: from test.rb:9:in `<main>' 1: from test.rb:6:in `outer' test.rb:2:in `inner': test (RuntimeError)
But on Ruby < 2.5, and on 3.0 again:
test.rb:2:in `inner': test (RuntimeError) from test.rb:6:in `outer' from test.rb:9:in `<main>'
- Notes:
- While redesigning the backtrace, command-line option
--backtrace-limit=<number-of-entries>
was introduced, so, you can run the example above this way to partially suppress backtrace:$ ruby --backtrace-limit=0 test.rb test.rb:2:in `inner': test (RuntimeError) ... 2 levels...
- While redesigning the backtrace, command-line option
Strings and symbols
Interpolated String literals are no longer frozen when # frozen-string-literal: true
is used
- Reason: The idea of
frozen-string-literal
pragma is to avoid unnecessary allocations, when the same"string"
repeated in the code called multiple times; but if the string is constructed dynamically with interpolation, there would be no point (it is hard to predict that allocations would be avoided, as the string can be different each time); it also breaks the “intuitive” feeling that the string is dynamic. - Discussion: Feature #17104
- Documentation:
doc/syntax/comments.rdoc
- Code:
# frozen-string-literal: true def render(title, body) result = "## #{title}" result << "\n" result << body end puts render('The Title', 'The body') # Ruby 2.7: in `render': can't modify frozen String (FrozenError) # Ruby 3.0: # ## The Title # The body
String
: always returning String
On custom classes inherited from String
, some methods previously were returning an instance of this class, and others returned String
. Now they all do the latter.
- Reason: While one might argue that
MyCoolString.new(" foo ").strip
should return an instance ofMyCoolString
, it actually creates a significant problem: String internals don’t know how to construct a new instance of the child class (imagine it has a constructor likeStringFromFile.new(content, filename:)
), and in Ruby versions before 3.0 they didn’t actually construct child classes with the constructor. It was a source of all kinds of subtle bugs. - Discussion: Bug #10845
- Affected methods:
#*
,#capitalize
,#center
,#chomp
,#chop
,#delete
,#delete_prefix
,#delete_suffix
,#downcase
,#dump
,#each_char
,#each_grapheme_cluster
,#each_line
,#gsub
,#ljust
,#lstrip
,#partition
,#reverse
,#rjust
,#rpartition
,#rstrip
,#scrub
,#slice!
,#slice
/#[]
,#split
,#squeeze
,#strip
,#sub
,#succ
/#next
,#swapcase
,#tr
,#tr_s
,#upcase
- Code:
class Buffer < String attr_reader :capacity def initialize(content, capacity:) super(content) @capacity = capacity end # some impl. end stripped = Buffer.new(' foo ', capacity: 100).strip stripped.class # Ruby 2.7: Buffer # Ruby 3.0: String stripped.capacity # Ruby 2.7: nil -- one might ask "why it haven't been copied from parent???" # Ruby 3.0: NoMethodError (undefined method `capacity' for "foo":String)
- Note: The same change was implemented for Array.
Symbol#name
The method returns a frozen string with the symbol’s representation.
- Reason: Producing non-frozen strings from
Symbol#to_s
might lead to a memory/performance overhead, for example, when large data structures are serialized. It was discussed that#to_s
should be changed to just return a frozen String, but it would create a backward compatibility problem in code looking like this:# Not the best, but existing in the wild way of constructing strings from data containing symbols {some: 'data'}.map { |k, v| k.to_s << ': ' << v }.join(', ') # If k.to_s returns a frozen string, it would throw FrozenError (can't modify frozen String: "some")
as a compromise, a new method was added.
- Discussion: Feature #16150
- Documentation:
Symbol#name
Collections
Array
: always returning Array
On custom classes inherited from Array
, some methods previously were returning an instance of this class, and others returned Array
. Now they all do the latter.
- Reason: See above, where the same changes for String are explained.
- Discussion: Bug #6087
- Affected methods:
#*
,#drop
,#drop_while
,#flatten
,#slice!
,#slice
/#[]
,#take
,#take_while
,#uniq
- Code:
class Nodes < Array attr_reader :parent def initialize(nodes, parent) super(nodes) @parent = parent end # some impl. end uniq = Nodes.new(['<tr>Name</tr>', '<tr>Position</tr>', '<tr>Name</tr>'], 'table').uniq uniq.class # Ruby 2.7: Nodes # Ruby 3.0: Array uniq.parent # Ruby 2.7: nil -- one might ask "why it haven't been copied from parent???" # Ruby 3.0: NoMethodError (undefined method `uniq' for ["<tr>Name</tr>", "<tr>Position</tr>"]:Array)
Array
: slicing with Enumerator::ArithmeticSequence
Enumerator::ArithmeticSequence
was introduced in 2.6 as an concept representing “sequence from b
to e
with step s
”. Since 3.0, Array#[]
and Array#slice
accept arithmetic sequence as a slicing argument.
- Discussion: Feature #16812
- Documentation:
Array#[]
- Code:
text_data = ['---', 'Jane', '---', 'John', '---', 'Yatsuke', '---'] text_data[(1..) % 2] # each second element # => ["Jane", "John", "Yatsuke"] # Note that unlike slicing with Range, slicing with ArithmeticSequence might raise RangeError: text_data[0..100] # => ["---", "Jane", "---", "John", "---", "Yatsuke", "---"] text_data[(0..100) % 2] # RangeError (((0..100).%(2)) out of range)
- Notes: Don’t forget to put
()
around range: without them,0..100 % 2
is actually(0)..(100 % 2)
, e.g.0..0
.
Hash#except
- Reason:
Hash#slice
(“only selected set of keys”) was added in Ruby 2.5, butHash#except
was missing at this point. Added for completeness, and because it has lot of pragmatic usages. - Discussion: Feature #15822
- Documentation:
Hash#except
,ENV.except
- Code:
h = {a: 1, b: 2} h.except(:b) # => {:a=>1} h.except(:c) # unknown key is not an error # => {:a=>1, :b=>2}
- Note: Unlike ActiveSupport, there’s no
Hash#except!
(like there is no#slice!
)
Hash#transform_keys
: argument for key renaming
Hash#transform_keys(from: to)
allows to rename keys in a DRY way.
- Discussion: Feature #16274
- Documentation:
Hash#transform_keys
- Code:
h = {name: 'Ruby', years: 25} h.transform_keys(name: :title, years: :age) # => {:title=>"Ruby", :age=>25} h.transform_keys(name: :title, site: :url) # => {:title=>"Ruby", :years=>25} -- not mentioned keys are copied as is, unknown keys ignored h.transform_keys(years: :name, name: :title) # => {:title=>"Ruby", :name=>25} -- note that the first rename wouldn't replace the :name key, because # first all renames are deduced, and then applied h.transform_keys(name: :lang_name) { |k| :"meta_#{k}" } # => {:lang_name=>"Ruby", :meta_years=>25} -- block, if passed, is used to process keys the hash doesn't mention h.transform_keys!(name: :title, years: :age) # bang version works, too h # => {:title=>"Ruby", :age=>25}
Hash#each
consistently yields a 2-element array to lambdas
Just fixes an inconsistency introduced by optimization many versions ago.
- Discussion: Bug #12706
- Code:
{a: 1}.each(&->(pair) { puts "pair=#{pair}" }) # 2.7 and 3.0: pair=[:a, 1] -- so, it is "yield 2 items as one argument" {a: 1}.each(&->(pair, redundant) { puts "pair=#{pair}, redudnant=#{redundant}" }) # 2.7: pair=a, redudnant=1 -- arguments accidentally behave like for regular proc (auto-unpacking) # 3.0: ArgumentError (wrong number of arguments (given 1, expected 2)) -- no unexpected auto-unpacking
Procs/lambdas
Symbol#to_proc
reported as lambda
- Reason: It was noted that the result of
&:something
behaves like a lambda, but its introspection methods are misleadingly reported it as a regular proc. While no big offense, it can be inconvenient when learning, and produce subtle bugs in complicated metaprogramming. - Discussion: Feature #16260
- Documentation: —
- Code:
def test_it(&block) p [block.lambda?, block.parameters] end # Regular proc test_it { |x| x.size } # => [false, [[:opt, :x]]] # Regular lambda test_it(&->(x) { x.size }) # => [true, [[:req, :x]]] # Symbol proc test_it(&:size) # Ruby 2.7: [false, [[:rest]]] # Ruby 3.0: [true, [[:req], [:rest]]]
The second one is more true, because it behaves like lambda: doesn’t auto-unpack parameters, and requires the first one:
Warning[:deprecated] = true proc { |x, y| x + y }.call([1, 2]) # => 3, parameters unpacked proc { |x| x.inspect }.call # => "nil", no error, parameter is optional lambda { |x, y| x + y }.call([1, 2]) # ArgumentError (wrong number of arguments (given 1, expected 2)) lambda { |x| x.inspect }.call # ArgumentError (wrong number of arguments (given 0, expected 1)) :+.to_proc.call([1, 2]) # ArgumentError (wrong number of arguments (given 0, expected 1)) :inspect.to_proc.call # ArgumentError (no receiver given)
Note that the last two errors was raised even on Ruby 2.7, so the behavior is not changed, just introspection made consistent.
Kernel#lambda
warns if called without a literal block
- Reason: One might expect that
lambda(&other_block)
will change block’s “lambdiness”. It is not true, and it was decided that this behavior shouldn’t change. Instead, to avoid errors, this code now warns. - Discussion: Feature #15973
- Documentation: —
- Code:
block = proc { |x| p x } processed = lambda(&block) # On 3.0: warning: lambda without a literal block is deprecated; use the proc without lambda instead processed.lambda? # => false processed.call # => "nil", it is really still not a lambda
- Follow-ups: 3.3: The warning was turned into an exception.
Proc#==
and #eql?
The method will return true for separate Proc instances if the procs were created from the same block.
- Reason: Lazy Proc allocation optimization (not creating
Proc
object while just propagating&block
further) introduced an inconvenience for some DSL libraries when what are “logically” two references to the same proc, would become two unrelated objects, which could lead to subtle bugs. - Discussion: Feature #14267
- Documentation:
Proc#==
- Code:
class SomeDSL attr_reader :before_block, :after_block def before(&block) @before_block = block end def after(&block) @after_block = block end def before_and_after(&block) before(&block) after(&block) end def before_and_after_are_same? @before_block == @after_block end end dsl = SomeDSL.new dsl.before_and_after { 'some code' } dsl.before_block.object_id == dsl.after_block.object_id # => true on Ruby 2.4: they were converted to Proc object in #before_and_after, and then this # object was propagated to #before/#after # => false on Ruby 2.5-3.0: block was propagated without converting to object, and converts to # two different objects in #before/#after while storing in variables p dsl.before_and_after_are_same? # => true on Ruby 2.4, because it is the same object # => false on Ruby 2.5-2.7 # => true on 3.0: the objects ARE different, but == implemented the way that they are equal if # they are produced from exactly the same block
Random::DEFAULT
behavior change
Random::DEFAULT
was an instance of the Random
class, used by Random.rand
/Kernel.rand
. The object was removed, and now Random::DEFAULT
is just an alias for Random
class itself (it has class-level methods same as instance-level methods), and Random.rand
creates a new random generator implicitly per ractor, on the first call in that ractor.
- Reason: With the introduction of the Ractors, global mutable objects can’t be used, and
Random::DEFAULT
was such object (its state have changed with each#rand
call); so now the default random generator is ractor-local, and is not exposed as a constant (it is decided that it would be too confusing to have a constant with the value different in each ractor). - Discussion: Feature #17322 (
Random::DEFAULT
becomes the synonym ofRandom
), Feature #17351 (deprecating the constant). - Documentation:
Random
- Code:
Warning[:deprecated] = true Random::DEFAULT # Ruby 2.7: => #<Random:0x005587d09fba20> # Ruby 3.0: # warning: constant Random::DEFAULT is deprecated # => Random -- just the same class Random.seed # Ruby 2.7: NoMethodError: undefined method `seed' for Random:Class # Ruby 3.0: => 77406376310392739943146667089721213130 # Seed is delegated to (now internal) per-Ractor random object, which is always the same in the same ractor 10.times.map { Random.seed }.uniq # => [77406376310392739943146667089721213130] # It is different in another ractor Ractor.new { p Random.seed } # => 95097031741178961937025250800360539515
- Notes:
- Follow-up: In 3.2, the constant was removed.
Filesystem and IO
Dir.glob
and Dir.[]
result sorting
By default the results are sorted, which can be turned off with sorted: false
- Reason:
Dir.glob
historically returns the results in the same order the underlying OS API does it (which is undefined on Linux). While it can be argued as “natural” (do what the OS does), it is inconvenient for most of the cases; and most of other languages and tools switched to sorting in the last years. - Discussion: Feature #8709
- Documentation:
Dir.glob
- Note:
Dir.glob('pattern', sort: false)
allows to return results in old (system) order; this might be useful when testing some OS behavior.
Concurrency
Ractors
A long-anticipated concurrency improvement landed in Ruby 3.0. Ractors (at some point known as Guilds) are fully-isolated (without sharing GVL on CRuby) alternative to threads. To achieve thread-safety without global locking, ractors, in general, can’t access each other’s (or main program/main ractor) data. To share data with each other, ractors rely on message passing/receiving mechanism, aka actor model (hence the name). The feature was in design and implementation for a long time, so we’ll not try to discuss it here in details or link to some “main” discussion, just provide a simple example and links for further investigation.
- Code:
The simplistic example of ractors in action (classic “server-client” ping-pong):
server = Ractor.new do puts "Server starts: #{self.inspect}" puts "Server sends: ping" Ractor.yield 'ping' # The server doesn't know the receiver and sends to whoever interested received = Ractor.receive # The server doesn't know the sender and receives from whoever sent puts "Server received: #{received}" end client = Ractor.new(server) do |srv| # The server ractor is passed to client, and available as srv puts "Client starts: #{self.inspect}" received = srv.take # The Client takes a message specifically from the server puts "Client received from " \ "#{srv.inspect}: #{received}" puts "Client sends to " \ "#{srv.inspect}: pong" srv.send 'pong' # The client sends a message specifically to the server end [client, server].each(&:take) # Wait till they both finish
This will output:
Server starts: #<Ractor:#2 test.rb:1 running> Server sends: ping Client starts: #<Ractor:#3 test.rb:8 running> Client received from #<Ractor:#2 rac.rb:1 blocking>: ping Client sends to #<Ractor:#2 rac.rb:1 blocking>: pong Server received: pong
- Documentation:
Ractor
(class documentation),doc/ractor.md
(design documentation). - Follow-up: In Ruby 3.1, ractor’s concept of what’s allowed to share was adjusted to include shareable class/module instance variables. Ractors are still considered experimental though.
Non-blocking Fiber
and scheduler
The improvement of inter-thread concurrency for IO-heavy tasks is achieved with non-blocking Fibers. When several long I/O operations should be performed, they can be put in separate non-blocking Fibers, and instead of blocking each other while waiting, they would be transferring the control to the fiber that can proceed, while others wait.
- Discussion: Feature #16786, Feature #16792 (Mutex belonging to Fiber)
- New and updated methods:
Fiber
:Fiber.set_scheduler
,Fiber.scheduler
,Fiber.new
(blocking: true/false)
parameter,Fiber#blocking?
,Fiber.blocking?
- Methods invoking scheduler:
Kernel.sleep
,IO#wait_readable
,IO#wait_writable
,IO#read
,IO#write
and other related methods (e.g.IO#puts
,IO#gets
),Thread#join
,ConditionVariable#wait
,Queue#pop
,SizedQueue#push
; IO#nonblock?
now defaults totrue
;Mutex
belongs toFiber
rather thanThread
(can be unlocked from other fiber than the one that locked it).
- Documentation:
Fiber
(class documentation),Fiber::SchedulerInterface
(definition of the interface which the scheduler must implement,doc/fiber.md
(design documentation). - Code:
require 'net/http' start = Time.now Thread.new do # in this thread, we'll have non-blocking fibers Fiber.set_scheduler Scheduler.new # see Notes about the scheduler implementation %w[2.6 2.7 3.0].each do |version| Fiber.schedule do # Runs block of code in a separate Fiber t = Time.now # Instead of blocking while the response will be ready, the Fiber will invoke scheduler # to add itself to the list of waiting fibers and transfer control to other fibers Net::HTTP.get('rubyreferences.github.io', "/rubychanges/#{version}.html") puts '%s: finished in %.3f' % [version, Time.now - t] end end end.join # At the END of the thread code, Scheduler will be called to dispatch all waiting fibers # in a non-blocking manner puts 'Total: finished in %.3f' % (Time.now - start) # Prints: # 2.6: finished in 0.139 # 2.7: finished in 0.141 # 3.0: finished in 0.143 # Total: finished in 0.146
Note that “total” is lower than sum of all fibers execution time: on
HTTP.get
, instead of blocking the whole thread, they were transferring control while waiting, and all three waits are performed in parallel. - Notes: The feature is somewhat unprecedented for Ruby in the fact that no default Scheduler implementation is provided. Implementing the Scheduler in a reliable way (probably using some additional non-blocking event loop library) is completely up to the user. Considering that the feature is implemented by Samuel Williams of the Async fame, the Async gem utilizes the new feature since the day of 3.0 release.
- Follow-up:s
- In Ruby 3.1, more scheduler hooks were added to make more core methods support asynchronous execution;
- In 3.2, even more hooks were added, and
SchedulerInteface
documentation abstraction was renamed toScheduler
.
Thread.ignore_deadlock
accessor
Disabling the default deadlock detection, allowing the use of signal handlers to break deadlock.
- Reason: The change helps with rare condition when Ruby’s internal deadlock detector is fooled by external signals.
- Discussion: Bug #13768
- Documentation:
Thread.ignore_deadlock=
,Thread.ignore_deadlock
- Code:
queue1 = Queue.new queue2 = Queue.new trap(:SIGCHLD) { queue1.push 'from SIGCHLD to queue1' } Thread.start { Process.spawn("/bin/sleep 1") } Thread.start { queue2.push("via Thread to queue2: #{queue1.pop}") } Thread.ignore_deadlock = true # <== New feature # Here the message would be received when the childprocess will finish, send message to queue1, # which then will be caught in the thread and sent to queue2. puts queue2.pop # Prints: "via Thread to queue2: from SIGCHLD to queue1" # # But Without the marked line, it will be printed instead: # in `pop': No live threads left. Deadlock? (fatal) # ... description of sleeping threads ...
Fiber#backtrace
& #backtrace_locations
Like similar methods of the Thread
, provides a locations of the currently executed Fiber code.
- Discussion: Feature #16815
- Documentation:
Fiber#backtrace
,Fiber#backtrace_locations
, - Code:
f = Fiber.new { Fiber.yield } # When fiber is not yet running, the backtrace is empty f.backtrace # => [] # When fiber was resumed, and then yielded control, you can ask about its location f.resume f.backtrace # => ["test.rb:1:in `yield'", "fbr.rb:1:in `block in <main>'"] f.backtrace_locations # => ["fbr.rb:1:in `yield'", "fbr.rb:1:in `block in <main>'"] # Despite looking the same, backtrace_locations are actually instances of Thread::Backtrace::Location loc = f.backtrace_locations.first loc.label # => "yield" loc.path # => "test.rb" loc.line # => 1 # Like Thread.backtrace_locations, the method accepts arguments: f.backtrace_locations(1) # start from 1 # => ["fbr.rb:1:in `block in <main>'"] f.backtrace_locations(0, 1) # start from 0, take 1 # => ["test.rb:1:in `yield'"] f.backtrace_locations(0...1) # ranges are acceptable, too # => ["test.rb:1:in `yield'"] f.resume # When the fiber is finished, there is no location f.backtrace_locations # => nil
Fiber#transfer
limitations changed
#transfer
is the method that allows fiber to pass control to other fiber directly, allowing several fibers to control each other, creating arbitrary directed graph of control. Two styles of passing control (Fiber.yield
/ Fiber#resume
vs Fiber#transfer
to and from fiber) can’t be freely mixed: the way in which fiber lost control should be the same it received the control back. In 2.7, this was implemented by a strict rule: once the fiber received control via #transfer
, it can never return back to .yield
/#resume
style. But in 3.0, better grained set of limitations was designed, so when the Fiber passed control via #transfer
and then received it back (via #transfer
in other fiber), it again can .yield
and be #resume
d.
- Reason: The new design makes the “entry point” fiber of
#transfer
graph accessible from outside the graph. - Discussion: Bug #17221
- Documentation:
Fiber#transfer
- Code:
require 'fiber' manager = nil # For local var to be visible inside worker block # This fiber would be started with transfer # It can't yield, and can't be resumed worker = Fiber.new { |work| puts "Worker: starts" puts "Worker: Performed #{work.inspect}, transferring back" # Fiber.yield # this would raise FiberError: attempt to yield on a not resumed fiber # manager.resume # this would raise FiberError: attempt to resume a resumed fiber (double resume) manager.transfer(work.capitalize) } # This fiber would be started with resume # It can yield or transfer, and can be transferred # back or resumed manager = Fiber.new { puts "Manager: starts" puts "Manager: transferring 'something' to worker" result = worker.transfer('something') puts "Manager: worker returned #{result.inspect}" # worker.resume # this would raise FiberError: attempt to resume a transferring fiber Fiber.yield # this is OK __since 3.0__, the fiber transferred back and from, now it can yield puts "Manager: finished" } puts "Starting the manager" manager.resume puts "Resuming the manager" # manager.transfer # this would raise FiberError: attempt to transfer to a yielding fiber manager.resume # This is possible __since 3.0__
This prints:
Starting the manager Manager: starts Manager: transferring 'something' to worker Worker: starts Worker: Performed "something", transferring back Manager: worker returned "Something" Resuming the manager Manager: finished
Before Ruby 3.0,
manager
Fiber (once it ran the loop oftransfer
s withworker
), had no way to return control generically, to whatever other Fiber wants to run. - Notes: The full list of the new rules are:
- Can’t
transfer
to the fiber resumed (e.g. at currently running due to receiving control viaresume
, and nottransfer
) - Can’t
transfer
to the fiber yielding (e.g. the one that runFiber.yield
and is waiting to beresume
d) - Can’t
resume
the fiber transferred (e.g. once the fibertransfer
to some other fiber, it can obtain the control back only viatransfer
) - Can’t
Fiber.yield
from the fiber that was never resumed (e.g. that was started due totransfer
)
- Can’t
Internals
GC.auto_compact
accessor
Setter/getter for the option to run GC.compact
(introduced in Ruby 2.7) on each major garbage collection. false
by default.
- Discussion: Feature #17176
- Documentation:
GC.auto_compact
,GC.auto_compact=
- Notes: It is noticed by feature authors that currently compaction adds a significant overhead to the garbage collection, so should be tested if that’s what you want.
Standard library
Set
: as parts of ongoing “Sets: need ♥️” initiative (Feature #16989), some adjustments were made:SortedSet
has been removed for dependency and performance reasons (it silently depended uponrbtree
gem).Set#join
is added as a shorthand for.to_a.join
. Discussion: Feature #16991.Set#<=>
: returns-1
/+1
if one set is the proper sub-/super-set of the other,0
if they are equal, andnil
otherwise:Set[1, 2, 3] <=> Set[2, 3] # => 1 Set[1, 2, 3] <=> Set[3, 4] # => nil # Note that atomic comparison operators already worked in 2.7 and earlier: Set[1, 2, 3] > Set[2, 3] # => true # But the new operator allows, for example, sorting of sub/super-sets: [Set[1, 2, 3], Set[1, 2]].sort # 2.7: ArgumentError (comparison of Set with Set failed) # 3.0: => [#<Set: {1, 2}>, #<Set: {1, 2, 3}>]
Discussion: Feature #16995.
- Libraries switched to version numbering corresponding to Ruby version (3.0.0) and made Ractor-compatible internally:
- Libraries made Ractor-compatible with no version numbering logic change:
- OpenStruct:
- Made Ractor-compatible;
- Several robustness improvements (initialization, allowed method names limitations, etc.), see discussions in Bug #12136, Bug #15409, Bug #8382.
- Despite the improvements, use of the library is officially discouraged due to performance, version compatibility, and potential security issues.
Network
- net/http
Net::HTTP#verify_hostname
accessor allows to skip hostname verification. Discussion: Feature #16555.Net::HTTP.get
,Net::HTTP.get_response
, andNet::HTTP.get_print
can take the request headers as a Hash in the second argument when the first argument is a URI. Discussion: Feature #16686.Net::HTTP.get(URI('http://www.example.com/index.html'), { 'Accept' => 'text/html' })
- net/smtp
- now has Server Name Indication (SNI) support.
Net::SMTP.start
is switched from positional to keyword arguments for all of them besides first two (address
andport
).- TLS should not check the host name by default.
Socket
- Add
:connect_timeout
keyword argument toTCPSocket.new
. Discussion: Feature #17187
- Add
- open-uri: redefinition of
Kernel#open
, deprecated in 2.7 in favor ofURI.open
, is removed. Discussion: Misc #15893.
Large updated libraries
- Bundler 2.2.3: Changelog
- RubyGems 3.2.3: Changelog
- CSV 3.1.9: Changelog
- Fiddle 1.0.6: Changelog
- IRB 1.2.6: No Changelog available
- JSON 2.4.1: Changelog
- Psych 3.3.0: No Changelog available
- Reline 0.1.5: No Changelog available
Standard library contents change
New libraries
Libraries related to type annotations:
Libraries promoted to default gems
stdgems.org project has a nice explanations of default and bundled gems concepts, as well as a list of currently gemified libraries.
“For the rest of us” this means libraries development extracted into separate GitHub repositories, and they are just packaged with main Ruby before release. It means you can do issue/PR to any of them independently, without going through more tough development process of the core Ruby.
Libraries extracted in 3.0:
- abbrev
- base64
- debug — no GitHub source available, not published on rubygems.org (?)
- digest
- drb
- English
- erb
- find
- io-nonblock
- io-wait
- net-ftp
- net-http
- net-imap
- net-protocol
- nkf
- open-uri
- optparse
- pathname
- pp
- prettyprint
- resolv
- resolv-replace
- rinda
- securerandom
- set
- shellwords
- syslog
- tempfile
- time
- tmpdir
- tsort
- un
- weakref
- win32ole
Libraries excluded from the standard library
Unsupported and lesser used libraries removed from the standard library, and now can be installed as a separate gems.
- net-telnet (was a bundled gem)
- xmlrpc (was a bundled gem)
- SDBM. Discussion: Bug #8446
- WEBrick. While having a simple webserver in the standard distribution can be considered a good thing (one can show “…and start a web server…” in some very basic Ruby tutorial), WEBrick was a huge maintenance burden, mainly due to the potential security issues. While nobody is advised to use WEBrick in production, any public report about its security issue was registered as a security issue in Ruby, requiring immediate attention to maintain Ruby’s reputation. And with RubyGems system in place, any tutorial as easy might advise to install any server it considers suitable. Discussion: Feature #17303.