Ruby Evolution
A very brief list of new significant features that emerged in Ruby programming language since version 2.0 (2013).
It is intended as a “bird eye view” that might be of interest for Ruby novices and experts alike, as well as for curious users of other technologies.
It is part of a bigger Ruby Changes effort, which provides a detailed explanations and justifications on what happens to the language, version by version. The detailed changelog currently covers versions since 2.4, and the brief changelog links to more detailed explanations for those versions (links are under version numbers at the beginning of the list items).
The choice of important features, their grouping, and depth of comment provided are informal and somewhat subjective. The author of this list is focused on the changes of the language as a system of thinking and its syntax/semantics more than on a particular implementation.
As Ruby is highly object-oriented language, most of the changes can be associated with some of its core classes. Nevertheless, a new method in one of the core classes frequently changes the way code could be written, not just adds some small convenience.
🇺🇦 🇺🇦 This work was started in mid-February, before the start of aggressive full-scale war Russia leads against Ukraine. I am finishing it after my daily volunteer work (delivering food through my district), why my homecity Kharkiv is still constantly bombed. Please care to read two of my appeals to Ruby community before proceeding: first, second.
The latest blog post dedicated to the reference creation also juxtaposes the evolution of the language with my personal history and history of my country.🇺🇦 🇺🇦
General changes
- 2.0 Refinements are introduced as experimental feature. It is meant to be a hygienic replacement for contextual extending of modules and classes. The feature became stable in 2.1, but still has questionable mindshare, so the further enhancements to it are covered in “deeper topics” section. Example of refinements usage:
# Without refinements: Extending core object to make writing some statistics-heavy report easier: class Numeric def percent_of(other) self.fdiv(other) * 100 end end # Usage: csv << [spent.percent_of(budget), debt.percent_of(budget), budget] # The problem is, Numeric#percent_of is now available in every other module, and depending on the # name and design, might cause problems in unrelated code # With refinements: module Stats refine Numeric do def percent_of(other) self.fdiv(other) * 100 end end end # The "refined" methods are available only in the file that explicitly uses them using Stats csv << [spent.percent_of(budget), debt.percent_of(budget), budget]
- 2.6 Non-ASCII constant names allowed
- 2.7 “Safe” and “taint” concepts are deprecated in general
- 3.0 Class variable behavior became stricter: top-level
@@variable
is prohibited, as well as overriding in descendant classes and included modules. - 3.0 Type definitions concept is introduced. 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. Example of type definition syntax:
class Dog attr_reader name: String def initialize: (name: String) -> void def bark: (at: Person | Dog | nil) -> String end
Expressions
- 2.3 Safe navigation operator:
s = 'test' s&.length # => 4 s = nil s&.length # => nil, instead of NoMethodError
- 2.4 Multiple assignment allowed in conditional expression
- 2.4 Toplevel
return
to stop interpreting the file immediately; useful for cases like platform-specific classes, where instead of wrapping the whole file inif SOMETHING_SUPPORTED...
, you can justreturn unless SOMETHING_SUPPORTED
at the beginning.
Pattern-matching
- 2.7
Pattern-matching
introduced as an experimental feature that allows to deeply unpack/check nested data structures:case config in version: 'legacy', username: # matches {version: 'legacy', username: anything} and puts value in `username` puts "Connect with user '#{username}'" in db: {user: } # matches {db: {user: anything}} and puts value in `user` puts "Connect with user '#{user}'" in String => username # matches when config is a String and puts it into `username` puts "Connect with user '#{username}'" else puts "Unrecognized structure of config" end
- 3.0
=>
pattern-matching expression introduced{a: 1, b: 2} => {a:} # -- deconstructs and assigns to local variable `a`; fails if pattern not matched long.chain.of.computations => result # can also be used as a "rightward assignment"
- 3.0
in
pattern-matching expression repurposed as atrue
/false
checkif {a: 1, b: 2} in {a:} # just "check if match", returning true/false; also deconstructs # ...
- 3.0 Find pattern is supported:
[*elements_before, <complicated pattern>, *elements_after]
- 3.1 Expressions and non-local variables allowed in pin operator
^
- 3.1 Parentheses can be omitted in one-line pattern matching:
{a: 1, b: 2} => a:
- 3.2 Deconstruction added to core and standard library objects:
MatchData#deconstruct
and#deconstruct_keys
,Time#deconstruct_keys
,Date#deconstruct_keys
,DateTime#deconstruct_keys
:'Ruby 3.2.0'.match(/Ruby (\d)\.(\d)\.(\d)/) => major, minor, patch major #=> "3" minor #=> "2" patch #=> "0" if Time.now in year: 2023, month: ..3, wday: 0..5 puts "Working day, first quarter!" end
Kernel
Kernel
is a module included in every object, providing most of the methods that look “top-level”, like puts
, require
, raise
and so on.
- 2.0
#__dir__
: absolute path to current source file - 2.0
#caller_locations
which returns an array of frame information objects, in a form of new classThread::Backtrace::Location
- 3.2
Thread.each_caller_location
as an efficient method to iterate through part of the call stack.
- 3.2
- 2.0
#caller
accepts second optional argumentn
which specify required caller size. - 2.2
#throw
raisesUncaughtThrowError
, subclass ofArgumentError
when there is no corresponding catch block, instead ofArgumentError
. - 2.3
#loop
: when stopped by aStopIteration
exception, returns what the enumerator has returned instead ofnil
- 2.5
#pp
debug printing method is available withoutrequire 'pp'
- 3.1
#load
allows to pass module as a second argument, to load code inside module specified
Object
Object
is a class most other classes are inherited from (save for very special cases when the BasicObject
is inherited). So the methods defined in Object
are available in most of the objects.
Unlike Kernel
’s method described above, Object
’s methods are public. E.g. every object has private #puts
from Kernel
that it can use inside its own methods, and every object has public #inspect
from Object
, that can be called by other objects.
- 2.0
#respond_to?
against a protected method now returnsfalse
by default, can be overrided byrespond_to?(:foo, true)
. - 2.0
#respond_to_missing?
,#initialize_clone
,#initialize_dup
became private. - 2.1
#singleton_method
- 2.2
#itself
introduced, just returning the object and making code like this easier:array_of_objects.group_by(&:itself)
- 2.6
#then
(initially introduced as 2.5#yield_self
) for chainable computation, akin to Elixir’s|>
:[BASE_URL, path].join('/') .then { |url| open(url).read } .then { |body| JSON.parse(body, symbolyze_names: true) } .dig(:data, :items) .then { |items| File.write('response.yml', items.to_yaml) }
Modules and classes
This section lists changes in how modules and classes are defined, as well as new/changed methods of core classes Module
and Class
. Note that most of module-level “keywords” we regularly use are actually instance methods of the Module
class:
class Foo
attr_reader :bar # it is a method Module#attr_reader
private # it is a method Module#private
include Enumerable # it is a method Module#include
def each # def is not a method, it is a real keyword!
# ...
end
define_method(:test, &block) # but it is a method Module#define_method
end
- 2.0
#prepend
introduced: like#include
, but adds prepended module to the beginning of the ancestors chain (also#prepended
and#prepend_features
hooks):class A < Array # Only adds new methods the class doesn't define itself include Enumerable def map puts "mapping" end end class B < Array # Goes in front of the class itself in ancestors chain, can redefine its methods prepend Enumerable def map puts "mapping" end end p A.ancestors # [A, Array, Enumerable, ...] p A.new([1, 2, 3]).map(&:to_s) # prints "mapping", returns nil p B.ancestors # [Enumerable, B, Array, ...] p B.new([1, 2, 3]).map(&:to_s) # returns ["1", "2", "3"]
- 2.0
#const_get
accepts a qualified constant string, e.g.Object.const_get("Foo::Bar::Baz")
- 2.1
#ancestors
- 2.1 The ancestors of a singleton class now include singleton classes, in particular itself.
- 2.1
#singleton_class?
- 2.1
#include
and#prepend
are now public methods, so one can doAnyClass.include AnyModule
without resorting tosend(:include, ...)
(which people did anyway) - 2.3
#deprecate_constant
- 2.5 methods for defining methods and accessors (like
#attr_reader
and#define_method
) became public - 2.6
#method_defined?
:inherit
argument - 2.7
#const_source_location
allows to query where some constant (including modules and classes) was first defined. - 2.7
#autoload?
:inherit
argument. - 3.0
#include
and#prepend
now affects modules that already include the receiver: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]]
- 3.0 Changes in return values/accepted parameters of several methods, making code like
private attr_reader :a, :b, :c
work (#attr_reader
started to return arrays of symbols, and#private
accepts arrays) - 3.1
Class#subclasses
- 3.1
Module#prepend
behavior changed to take effect even if the same module is already included. - 3.1
#private
and other visibility methods return their arguments, to allow usage in macros likememoize private def my_method...
- 3.2
Class#attached_object
for singleton classes. - 3.2
Module#const_added
hook method. - 3.2
Module#undefined_instance_methods
- 3.2 Behavior of module reopening/redefinition with included modules changed: top-level ones wouldn’t conflict with included anymore:
require 'net/http' include Net # Ruby 3.1: Reopens Net::HTTP # Ruby 3.2: Defines new top-level class HTTP class HTTP end
- 3.3
Module#set_temporary_name
to set a human-readable name for a module without assigning it to a constant.
Methods
This section lists changes in how methods are defined and invoked, as well as new/changed methods of core classes Method
and UnboundMethod
. Note: some of the behavior of method definition APIs in context of containing modules is covered in the above section about modules.
- 2.0 Keyword arguments. Before Ruby 2.0, keyword arguments could’ve been imitated to some extent with last hash argument without parenthises. In Ruby 2.0, proper keyword arguments were introduced. At first, they could only be optional (default value should’ve always been defined):
# before Ruby 2.0: def render(data, options = {}) indent = options.fetch(:indent, 2) separator = options.fetch(:separator) # imitation of mandatory arg., will raise if not present # ... end # calling: looks like separate argument due to Ruby allowing to omit {} render(something, indent: 4, separator: "\n\n") # Ruby 2.0: def render(data, indent: 2, separator: nil) raise ArgumentError, "separator is not defined" if separator.nil? # mandatory arguments should still be imitated
- 2.1 Required keyword arguments introduced:
def render(data, separator:, indent: 2) # will raise if `separator:` argument is not passed
- 2.1 Required keyword arguments introduced:
- 2.0 top-level
define_method
which defines a global function. - 2.1
def
now returns the symbol of its name instead ofnil
. Usable to use in class-level “macros” method:# before: def foo end private :foo # after: private def foo # `private` will receive :foo that `def` returned end
Module#define_method
andObject#define_singleton_method
also return the symbols of the defined methods, not the methods/procs
- 2.2
Method#curry
:writer = File.method(:write).curry(2).call('test.txt') # curry with 2 arguments, supply first of them # Now, the `writer` can be used as a 1-argument callable object: writer.call('content') # Invokes File.write('test.txt', 'content')
- 2.2
Method#super_method
- 2.5
Method#===
, allowing to use it ingrep
andcase
:require 'prime' (1..50).grep(Prime.method(:prime?)) #=> [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
- 2.7
self.<private_method>
is allowed - 2.7 Big Keyword Argument Separation: some incompatibilities were introduced by need, so the distinction of keyword arguments and hashes in method arguments was more clear, handling numerous irritating edge cases.
- 2.7 Introduce argument forwarding with
method(...)
syntax. As after the keyword argument separation “delegate everything” syntax became more complicated (you need to use and pass(*args, **kwargs)
, because just*args
wouldn’t always work), simplified syntax was introduced:def wrap_log(...) # this is literal code that can be used now, not a placeholder for a demo puts "Logging at #{Time.now}" log.call(...) end wrap_log(:info, "Foo", context: some_context) # both positional and keyword args are passed successfully
- 2.7 Better
Method#inspect
with signature and source code location - 2.7
UnboundMethod#bind_call
- 3.0 Arguments forwarding (
...
)supports
leading arguments - 3.0 “
Endless
” (one-line) method definition:def square(n) = n**n
- 3.1
Method#private?
,#protected?
,#public?
, same are defined forUnboundMethod
- 3.2 The change was reverted.
- 3.1 Values in keyword arguments can be omitted:
x = 100 p(x:) # same as p(x: x), prints: {:x => 100}
- 3.1 Anonymous block argument:
def logged_open(filename, &) puts "Opening #{filename}..." File.open(filename, &) end
- 3.2 Anonymous keyword and positional arguments forwarding (Methods: Array/Hash Argument):
# Only accepts positional arguments and passes them further def log(level, *) = logger.log(level, *) # Only accepts anonymous keyword args and passes them further def get(url, **) = send_request(:get, url, **)
- 3.3 Anonymous parameters forwarding inside blocks are disallowed, when the block also has anonymous parameters.
Procs, blocks and Proc
class
- 2.0 removed
Proc#==
and#eql?
so two procs are equal only when they are the same object. - 2.2
ArgumentError
is no longer raised when lambdaProc
is passed as a block, and the number of yielded arguments does not match the formal arguments of the lambda, if just an array is yielded and its length matches. - 2.6
Proc
composition with>>
and<<
:PROCESSOR = proc { |str| '{' + str + '}' } >> :upcase.to_proc >> method(:puts) %w[test me please].map(&PROCESSOR) # prints # {TEST} # {ME} # {PLEASE}
- 2.7 Numbered block parameters:
[1, 2, 3].map { _1 * 100 } # => 100, 200, 300
- 3.2
Proc#parameters
: new keyword argumentlambda: true/false
, improving introspection of whether arguments have default values or they are just optional because allproc
arguments are. - 3.3 Added a warning that since 3.4,
it
would become a synonym for_1
.
Comparable
Included in many classes to implement comparison methods. Once class defines a method #<=>
for object comparison (returning -1
, 0
, 1
, or nil
) and includes Comparable
, methods like ==
, <
, <=
etc. are defined automatically. Changes in Comparable
module affect most of comparable objects in Ruby, including core ones like numbers and strings.
- 2.3
#==
no longer rescues exceptions (so if owner class’<=>
raises, the user will see original exception) - 2.4
#clamp
:123.clamp(50, 100) # => 100 23.clamp(50, 100) # => 50 53.clamp(50, 100) # => 53
- 2.7
#clamp
supportsRange
argument:123.clamp(0..100) # one-sided clamp with endless/beginless ranges work too! -123.clamp(0..) #=> 0 123.clamp(..100) #=> 100
Numeric
- 2.1
Fixnum#bit_length
,Bignum#bit_length
- 2.1 Added suffixes for integer and float literals:
r
,i
, andri
:1/3r # => (1/3), Rational 2 + 5i # => (2 + 5i), Complex
- 2.2
Float#next_float
,#prev_float
- 2.3
Numeric#positive?
and#negative?
- 2.4
Fixnum
andBignum
are unified intoInteger
- 2.4
Numeric#infinite?
and#finite?
- 2.4
Integer#digits
- 2.4 Rounding methods
Numeric#ceil
,Numeric#floor
,Numeric#truncate
:ndigits
optional argument. - 2.4
Integer#round
andFloat#round
:half:
argument - 2.5
Integer#pow
:modulo
argument - 2.5
Integer#allbits?
,#anybits?
,#nobits?
# classic way of checking some flags: (object.flags & FLAG_ADMIN) > 0 # new way: object.flags.anybits?(FLAG_ADMIN)
- 2.5
Integer.sqrt
- 2.7
Integer#[]
supports range of bits - 3.1
Integer.try_convert
- 3.2
Integer#ceildiv
Strings, symbols, regexps, encodings
- 2.0 Big encoding cleanup:
- Default source encoding is changed to UTF-8 (was US-ASCII)
- Iconv has been removed from standard library; core methods like
String#encode
andString#force_encoding
(introduced in 1.9) should be preferred
- 2.0
%i
symbol array literals shortcut:%i[first_name last_name age] # => [:first_name, :last_name, :age]
- 2.0
String#b
to set string encoding as ASCII-8BIT (aka “binary”, raw bytes). - 2.1
String#scrub
and#scrub!
to verify and fix invalid byte sequence. - 2.2 Most symbols which are returned by
String#to_sym
are garbage collectable. While it might be perceived as an implementation detail, it means also the change in language use: there is no need to avoid symbols where they are more expressive, even if there are a lot of them. - 2.2
String#unicode_normalize
,#unicode_normalize!
, and#unicode_normalized?
- 2.3
<<~
HERE-document literal (removing the leading spaces):text = <<~HERE The text, indented for readability. No leading spaces please. HERE p text # => "The text, indented for readability.\nNo leading spaces please.\n"
- 2.3
String.new
accepts keyword argumentencoding:
- 2.4 Case conversions (
String#downcase
,String#upcase
, and other related methods) fully support Unicode:'Straße'.upcase # => 'STRASSE' 'İzmir'.upcase(:turkic) # => İZMİR -- locale-specific case conversion
- 2.4
String.new
:capacity:
argument to pre-allocate memory if it is known the string will grow - 2.4
String#casecmp?
,Symbol#casecmp?
as a more expressive version of#casecmp
when boolean value is needed (#casecmp
returns-1
/0
/1
):'FOO'.casecmp?('foo') # => true 'Straße'.casecmp?('STRASSE') # => true, Unicode-aware
- 2.4
String#concat
and#prepend
accept multiple arguments - 2.4
String#unpack1
as a shortcut to"foo".unpack(...).first
- 2.4
Regexp#match?
,String#match?
, andSymbol#match?
for when it is only necessary to know “if it matches or not”. Unlike=~
and#match
, the methds don’t alocateMatchData
instance, which might make the check more efficient. - 2.4
MatchData
: better support for named captures:#named_captures
,#values_at
- 2.5
String#delete_prefix
and#delete_suffix
- 2.5
String#grapheme_clusters
and#each_grapheme_cluster
- 2.5
String#undump
deserialization method, symmetric to#dump
- 2.5
String#start_with?
accepts a regexp (but not#end_with?
) - 2.5
Regexp
: absence operator(?~<pattern>)
: match everything except this particular pattern - 2.6
String#split
supports block:"several\nlong\nlines".split("\n") { |part| puts part if part.start_with?('l') } # prints: # long # lines # => "several\nlong\nlines"
- 2.7
Symbol#end_with?
and#start_with?
as a part of making symbols as convenient as strings, while maintaining their separate meaning - 3.1
String#unpack
and#unpack1
addedoffset:
argument, to unpack data from the middle of a stream. - 3.1
MatchData#match
andMatchData#match_length
- 3.2 Introduced several new byte-oriented methods:
String#byteindex
,String#byterindex
,String#bytesplice
, andMatchData#byteoffset
. - 3.2
Regexp.new
: passing flags as a string is supported:Regexp.new('foo', 'im') #=> /foo/im
- 3.2 ReDoS vulnerability prevention:
Regexp.timeout
,Regexp.timeout=
,Regexp.new
(timeout:
keyword argument),Regexp.linear_time?
. - 3.3
String#bytesplice
: additional arguments to select a portion of the inserted string. - 3.3
MatchData#named_captures
:symbolize_names:
argument.
Struct
- 2.5 Structs
initialized
by keywords:User = Struct.new(:name, :email, keyword_init: true) User.new(name: 'Matz', email: 'matz@ruby-lang.org')
- 3.1 Warning on passing keywords to a non-keyword-initialized struct
- 3.1
Struct.keyword_init?
- 3.2
Struct.new
accepts both positional and keyword arguments by default, unlesskeyword_init: true
orfalse
was explicitly specified.
Data
- 3.2
Data
: new immutable value object class introduced. It has a stricter and leaner interface thanStruct
:Point = Data.define(:x, :y) # Both positional and keyword arguments can be used p1 = Point.new(1, 0) #=> #<data Point x=1, y=0> p2 = Point.new(x: 0, y: 1) #=> #<data Point x=0, y=1> # all arguments are mandatory Point.new(1) # missing keyword: :y (ArgumentError) # there is no setters or any other way to change already created object p1.x = 5 # undefined method `x=' for #<data Point x=1, y=0> (NoMethodError) p1.instance_variable_set('@z', 100) # can't modify frozen Point: #<data Point x=1, y=0> (FrozenError) # #with method can be used to construct new instances, # replacing only parts of the data: p1.with(y: 100) #=> #<data Point x=1, y=100>
Time
- 2.5
Time.at
supports units - 2.6 Support for
timezones
. The timezone object should be provided by external library; expectation of its API matches the most popular tzinfo:require 'tzinfo' zone = TZInfo::Timezone.get('America/New_York') time = Time.new(2018, 6, 1, 0, 0, 0, zone) time.zone # => #<TZInfo::DataTimezone: America/New_York> time.strftime('%H:%M %Z') # => "00:00 EDT" time.utc_offset # => -14400 = -4 hours time += 180 * 24*60*60 # + 180 days, summery->winter transition time.utc_offset # => -18000, -5 hours -- daylight saving handled by timezone
- 2.7
Time#floor
and#ceil
- 3.1
.new
,.at
, and.now
:in: time_zone_or_offset
parameter for constructing timeTime.now(in: TZInfo::Timezone.get('America/New_York')) # => 2022-07-09 06:25:06.162617846 -0400 Time.new(2022, 7, 1, 14, 30, in: '+05:00') # => 2022-07-01 14:30:00 +0500
- 3.2
Time.new
can parse a string (stricter and more robust thanTime.parse
of the standard library)
Enumerables, collections, and iteration
- 2.0 A decision was made to make a clearer separation of methods returning enumerators to methods calculating the value and returning array immediately, namely:
String#lines
,#chars
,#codepoints
,#bytes
now return arrays instead of an enumerators (methods for returning enumerators are#each_line
,#each_char
and so on).IO#lines
,#bytes
,#chars
and#codepoints
are deprecated in favor of#each_line
,#each_byte
and so on.
- 2.0 Binary search introduced in core with
Range#bsearch
andArray#bsearch
. - 2.3
#dig
introduced (inArray
,Hash
, andStruct
) for atomic nested data navigation:data = { status: 200 body: {users: [ {id: 1, name: 'Victor'}, {id: 2, name: 'Yuki'}, ]} } data.dig(:body, :users, 1, :name) #=> 'Yuki'
Numeric iteration
- 2.1
Numeric#step
allows the limit argument to be omitted, producingEnumerator
. Keyword argumentsto
andby
are introduced for ease of use:1.step(by: 5) # => #<Enumerator: 1:step({:by=>5})> 1.step(by: 5).take(3) #=> [1, 6, 11]
- 2.6
Enumerator::ArithmeticSequence
is introduced as a type returned byRange#step
andNumeric#step
:1.step(by: 5) # => (1.step(by: 5)) -- more expressive representation than above (1..200).step(3) # => ((1..200).step(3)) # It is also more powerful than generic Enumerator, as there is more knowledge about # the nature of the sequence: (1..200).step(3).last(2) # => [196, 199]
- 2.6
Range#%
alias forRange#step
for expressiveness:(1..10) % 2
producesArithmeticSequence
with meaning “from 1 to 10, each second element”; since Ruby 3.0, this can be used to slicing arrays:(0..) % 3 letters = ('a'..'z').to_a letters[(0..) % 3] #=> ["a", "d", "g", "j", "m", "p", "s", "v", "y"]
Enumerable
and Enumerator
- 2.0 The concept of lazy enumerator introduced with
Enumerable#lazy
andEnumerator::Lazy
:# If source is very large or has side effects like network reading, the following code will # first read it all, then produce intermediate array on each step source.select { some_condition }.map { some_transformation }.first(3) # while this code will just stack together operations, and then produce items one by one, till # the first 3 results are received: # vvvv source.lazy.select { some_condition }.map { some_transformation }.first(3)
- 2.0
Enumerator#size
for on-demand size calculation when possible. The code that creates Enumerator, might passsize
argument toEnumerator.new
(value or a callable object) if it can calculate the amount of objects to enumerate.Range#size
added, returning non-nil
value only for integer ranges
- 2.2
Enumerable#slice_after
and#slice_when
- 2.2
Enumerable#min
,#min_by
,#max
and#max_by
support optional argument to return multiple elements:[1, 6, 7, 2.3, -100].min(3) # => [-100, 1, 2.3]
- 2.3
Enumerable#grep_v
and#chunk_while
- 2.4
Enumerable#sum
as a generalized shortcut forreduce(:+)
; might be redefined in descendants (likeArray
) for efficiency. - 2.4
Enumerable#uniq
- 2.5
Enumerable#all?
,#any?
,#none?
, and#one?
accept patterns (any objects defining#===
):objects.all?(Numeric) ages.any?(18..60) strings.none?(/admin/i)
- 2.6
Enumerator
chaining withEnumerator#+
andEnumerable#chain
, producingEnumerator::Chain
:# Take data from several sources, abstracted into enumerator, fetching it on demand sources = URLS.lazy.map { |url| open(url).read } .chain(LOCAL_FILES.lazy.map { |path| File.read(path) }) # ...then uniformly search several sources (lazy-loading them) for some value sources.detect { |body| body.include?('Ruby 2.6') }
- 2.6
Enumerable#filter
/#filter!
as alias for#select
/#select!
(as more familiar for users coming from other languages) - 2.7
Enumerator.produce
to convert loops into enumerators:# Classic loop: date = Date.today date += 1 until date.monday? # With Enumerator.produce: Enumerator.produce(Date.today) { |date| date + 1 }.find(&:monday?)
- 2.7
Enumerable#filter_map
- 2.7
Enumerable#tally
method to count stats (hash of{object => number of occurrences in the enumerable}
) - 2.7
Enumerator::Lazy#eager
- 2.7
Enumerator::Yielder#to_proc
- 3.1
Enumerable#compact
- 3.2
Enumerator.product
introduced to create a cross-product ofEnumerable
-alike objects.
Range
- 2.6 Endless range:
(1..)
- 2.6
#===
uses#cover?
instead of#include?
which means that ranges can be used incase
andgrep
for any types, just checking if the value is between range ends:case DateTime.now when Date.new(2022)..Date.new(2023) # wouldn't match in Ruby 2.5, would match in Ruby 2.6
- 2.6
#cover?
accepts range argument - 2.7 Beginless range:
(...100)
- 3.3
Range#reverse_each
(specialized form ofEnumerable#reverse_each
) - 3.3
Range#overlap?
Array
- 2.0
#shuffle
and#sample
:random:
optional parameter that accepts random number generator, will be called withmax
argument. - 2.3
#bsearch_index
- 2.4
#concat
takes multiple arguments - 2.4
#pack
:buffer:
keyword argument to provide target - 2.5
#append
and#prepend
- 2.6
#union
and#difference
- 2.7
#intersection
- 3.1
#intersect?
Hash
- 2.0 Introduced convention of
#to_h
method for explicit conversion to hashes, and added it toHash
,nil
, andStruct
;- 2.1
Array#to_h
andEnumerable#to_h
were added. - 2.6
#to_h
accepts a block to define conversion logic:users.to_h { |u| [u.name, u.admin?] } # => {"John" => false, "Jane" => true, "Josh" => false}
- 2.1
- 2.0
Kernel#Hash
, invoking argument’s#to_hash
implicit conversion method, if it has one. - 2.2 Change overriding policy for duplicated key:
{**hash1, **hash2}
contains values ofhash2
for duplicated keys. - 2.2 Hash literal: Symbol key followed by a colon can be quoted, allowing code like
{"data-key": value}
or{"#{prefix}_data": value}
. - 2.3
#fetch_values
: a multi-key version of#fetch
- 2.3
#<
,#>
,#<=
,#>=
to check for inclusion of one hash in another. - 2.3
#to_proc
:ATTRS = {first_name: 'John', last_name: 'Doe', gender: 'Male', age: 27} %i[first_name age].map(&ATTRS) # => ['John', 27]
- 2.4
#compact
and#compact!
to dropnil
values - 2.4
#transform_values
and#transform_values!
- 2.5
#transform_keys
and#transform_keys!
- 2.5
#slice
- 2.6
#merge
supports multiple arguments - 3.0
#except
- 3.0
#transform_keys
: argument for key renaming{first: 'John', last: 'Doe'}.transform_keys(first: :first_name, last: :last_name) #=> {:first_name => 'John', :last_name => 'Doe'}
- 3.1 Values in Hash literals can be omitted:
x = 100 y = 200 {x:, y:} # => {x: 100, y: 200}, same as {x: x, y: y}
Set
Set
was a part of the standard library, but since Ruby 3.2 it became part of Ruby core. A more efficient implementation (currently Set
is implemented in Ruby, and stores data in Hash
inside), and a separate set literal is up for discussion. That’s why we list Set
’s changes are listed briefly here.
- 2.1
#intersect?
and#disjoint?
- 2.4
#compare_by_identity
and#compare_by_identity?
- 2.5
#===
as alias to#include?
, soSet
can be used ingrep
andcase
:file_list.grep(Set['README.md', 'License.txt']) # find an item that matches any of sets elements
- 2.5
#reset
- 3.0
SortedSet
(that was a part ofset
standard library before) has been removed for dependency and performance reasons (it silently depended uponrbtree
gem). - 3.0
#join
is added as a shorthand for.to_a.join
. - 3.0
#<=>
generic comparison operator (separate operators like#<
or#>
have been worked in previous versions, too) - 3.2
Set
became a built-in class - 3.3
Set#merge
accepts multiple arguments.
Other collections
- 2.0
ObjectSpace::WeakMap
introduced - 2.3
Thread::Queue#close
is added to notice a termination - 2.7
ObjectSpace::WeakMap#[]=
now accepts non-GC-able objects - 3.1
Thread::Queue.new
allows initial queue content to be passed - 3.2
Thread::Queue#pop
,Thread::SizedQueue#pop
, andThread::SizedQueue#push
havetimeout:
argument. - 3.3
ObjectSpace::WeakKeyMap
introduced - 3.3
ObjectSpace::WeakMap#delete
- 3.3
Thread::Queue#freeze
andThread::SizedQueue#freeze
raiseTypeError
.
Filesystem and IO
- 2.1
IO#seek
improvements: supportsSEEK_DATA
andSEEK_HOLE
, and symbolic parameters (:CUR
,:END
,:SET
,:DATA
,:HOLE
) for 2nd argument. - 2.1
IO#read_nonblock
and#write_nonblock
accepts optionalexception: false
to return symbols - 2.2
Dir#fileno
- 2.2
File.birthtime
,#birthtime
, andFile::Stat#birthtime
- 2.3
File.mkfifo
- 2.3 New
flags/constants
for IO opening:File::TMPFILE
(open anonymous temp file) andFile::SHARE_DELETE
(open file that is allowed to delete) - 2.3
IO.new
: new keyword argumentflags:
- 2.4
chomp:
option for string splitting:File.readlines("test.txt") # => ["foo\n", "bar\n", "baz\n"] File.readlines("test.txt", chomp: true) # => ["foo", "bar", "baz"]
- 2.4
Dir#empty?
,File#empty?
, andPathname#empty?
- 2.5
IO#pread
andIO#pwrite
- 2.5
IO#write
accepts multiple arguments - 2.5
File.open
better supportsnewline:
option - 2.5
File.lutime
- 2.5
Dir.children
and.each_child
- 2.6
#children
and#each_child
(instance method counterparts)
- 2.6
- 2.5
Dir.glob
:base:
argument allows to provide a directory to look into instead of constructing a glob string including it. - 2.6 New IO open mode
'x'
: combined with'w'
(open for writing), requests that file didn’t exist before opening. - 2.7
IO#set_encoding_by_bom
- 3.1
File.dirname
: optionallevel
to go up the directory tree - 3.1
IO::Buffer
low-level class introduced - 3.2 Support for timeouts for blocking IO via
IO#timeout=
. - 3.2 Generic
IO#path
that can be assigned oncreation
. - 3.3
Dir.for_fd
andDir.fchdir
. - 3.3
Dir#chdir
. - 3.3 Create subprocesses with
IO.read('| command')
and similar methods is deprecated.
Exceptions
This section covers exception raising/handling behavior changes, as well as changes in particular core exception classes.
- 2.0
LoadError#path
method to return the file name that could not be loaded. - 2.1
Exception#cause
provides the previous exception which has been caught at where raising the new exception. - 2.3
NameError#receiver
stores an object in context of which the error have happened. - 2.3
NameError
andNoMethodError
suggest possible fixes with did_you_mean gem:'test'.szie # NoMethodError: undefined method `szie' for "test":String # Did you mean? size
- 2.5
rescue
/else
/ensure
are allowed inside blocks:# before Ruby 2.5: %w[1 - 3].map do |num| begin Integer(num) rescue 'N/A' end end # Ruby 2.5+: %w[1 - 3].map do |num| Integer(num) rescue 'N/A' end
- 2.5
Exception#full_message
- 2.5
KeyError
:#receiver
and#key
methods - 2.5 New class:
FrozenError
- 2.5 Don’t hide coercion errors in
Numeric
andRange
operations: raise original exception and not “can’t be coerced” or “bad value for range” - 2.6
else
in exception-handling context without anyrescue
is prohibited. - 2.6
#Integer()
and other similar conversion methods now have optional argumentexception: true/false
, defining whether to raise error on input that can’t be converted or just returnnil
- 2.6
#system
: optional argumentexception: true/false
- 2.6 New arguments:
receiver:
forNameError.new
andNoMethodError.new
;key:
forKeyError.new
. It allows user code to construct errors with the same level of detail the language can. - 2.6
Exception#full_message
: formatting optionshighlight:
andorder:
added - 2.7
FrozenError#new
: receiver argument - 3.1
Thread::Backtrace.limit
reader to get the maximum backtrace size set with--backtrace-limit
command-line option - 3.2
Exception#detailed_message
to separate the original error message and possible contextual additions. - 3.2
SyntaxError#path
Warnings
- 2.0
Kernel#warn
accepts multiple args in like#puts
. - 2.4
Warning
module introduced - 2.5
Kernel#warn
callsWarning.warn
internally - 2.5
Kernel#warn
:uplevel:
keyword argument allows to tune which line to specify in warning message as a source of warning - 2.7
Warning::[]
andWarning::[]=
to choose which categories of warnings to show; the categories are predefined by Ruby and only can be:deprecated
or:experimental
(or none)- 3.0 User code allowed to specify category of its warnings with
Kernel#warn
and intercept the warning categoryWarning#warn
withcategory:
keyword argument; the list of categories is still closed.
- 3.0 User code allowed to specify category of its warnings with
- 3.3 New
Warning
category
::performance
.
Concurrency and parallelism
Thread
- 2.0 Concept of thread variables introduced: methods
#thread_variable_get
,#thread_variable_set
,#thread_variables
,#thread_variable?
. Note that they are different from variables available viaThread#[]
, which are fiber-local. - 2.0
.handle_interrupt
to setup handling on exceptions and.pending_interrupt?
/#pending_interrupt?
- 2.0
#join
and#value
now raises aThreadError
if target thread is the current or main thread. - 2.0 Thread-local
#backtrace_locations
- 2.3
#name
and#name=
- 2.4
.report_on_exception
/.report_on_exception=
and#report_on_exception
/#report_on_exception=
- 2.5
#fetch
is toThread#[]
likeHash#fetch
is toHash#[]
: it allows to reliably get Fiber-local variable, raising or providing default value when it isn’t defined - 3.0
.ignore_deadlock
/.ignore_deadlock=
- 3.1
#native_thread_id
Process
- 2.0
.getsid
for getting session id (unix only). - 2.1
.argv0
returns the original value of$0
. - 2.1
.setproctitle
sets the process title without affecting$0
. - 2.1
.clock_gettime
and.clock_getres
- 2.5
Process.last_status
as an alias of$?
- 3.1
Process._fork
- 3.3
Process.warmup
Fiber
- 2.0
#resume
cannot resume a fiber which invokes#transfer
. - 2.2
callcc
is obsolete, andFiber
should be used - 2.7
#raise
- 3.0 Non-blocking
Fiber
andFiber::SchedulerInterface
. This is a big and important change, see detailed explanation and code examples in 3.0’s changelog. In brief, Ruby code now can perform non-blocking I/O concurrently from several fibers, with no code changes other than setting a fiber scheduler, which should be implemented by a third-party library.- 3.1 New hooks for fiber scheduler:
#address_resolve
,#timeout_after
,#io_read
, and#io_write
- 3.2
Fiber::SchedulerInterface
is renamed toFiber::Scheduler
for documetation purposes; - 3.2
#io_select
hook added
- 3.1 New hooks for fiber scheduler:
- 3.0
#backtrace
and#backtrace_locations
- 3.2 Storage concept:
.[]
,.[]=
,#storage
, and#storage=
- 3.3
Fiber#kill
Ractor
- 3.0
Ractors
introduced. 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. - 3.1 Ractors can access module instance variables
Debugging and internals
Binding
Binding
object represents the execution context and allows to pass it around.
- 2.1
#local_variable_get
,#local_variable_set
,#local_variable_defined?
. Besides other things, it allows to use variables with names of Ruby reserved words:def do_something(if:) # you can name argument this way, but can't refer to it in method's body by name condition = binding.local_variable_get('if') # ...use condition somehow end # The syntax might be useful for DSLs like validate :foo, if: -> { condition }
- 2.2
#local_variables
- 2.2
- 2.2
#receiver
- 2.6
#source_location
- 3.2
Kernel#binding
raises if accessed not from Ruby
GC
Note: in the spirit of the rest of this reference, this section only describes the changes in a garbage collector API, not changes of CRuby GC’s algorithms.
- 2.0
GC::Profiler.raw_data
- 2.2
.latest_gc_info
returns:state
to represent current GC status. - 2.2 Rename
.stat
entries - 2.7
.compact
- 3.0
.auto_compact
and.auto_compact=
- 3.1 Measuring total time spent in GC:
.measure_total_time
,.measure_total_time=
,.stat
output updated,.total_time
added - 3.2
GC.latest_gc_info
: addneed_major_gc:
key
TracePoint
- 2.0
TracePoint
class is introduced: a fully object-oriented execution tracing API; it is a replacement of the deprecatedset_trace_func
. - 2.4
#callee_id
- 2.6
#parameters
- 2.6
:script_compiled
event (TracePoint: Events (though new event seems to be omitted),TracePoint#instruction_sequence
,TracePoint#eval_script
) - 2.6
#enable
: new paramstarget:
andtarget_line:
- 3.1
.allow_reentry
- 3.2
TracePoint#binding
returnsnil
forc_call
/c_return
- 3.2
TracePoint#enable
with a block default to trace the current thread. - 3.3
TracePoint
supports:rescue
event.
RubyVM::AbstractSyntaxTree
- 2.6
RubyVM::AbstractSyntaxTree
introduced - 3.2
.parse
:error_tolerant: true
option for parsing - 3.2
.parse
:keep_tokens: true
option for parsing, allowing access toNode#tokens
andNode#all_tokens
.
RubyVM::InstructionSequence
InstructionSequence
is an API to interact with Ruby virtual machine bytecode. It is implementation-specific.
- 2.0
.of
to get the instruction sequence from a method or a block. - 2.0
#path
,#absolute_path
,#label
,#base_label
and#first_lineno
to retrieve information from where the instruction sequence was defined. - 2.3
#to_binary
- 2.3
.load_from_binary
and.load_from_binary_extra_data
- 2.5
#each_child
,#trace_points
ObjectSpace
- 3.2
ObjectSpace#dump_all
allow to dump object shapes, a concept introduced in Ruby 3.2.
Deeper topics
Refinements
Refinements are hygienic replacement for reopening of classes and modules. They allow to add methods to objects on the fly, but unlike reopening classes (known as “monkey-patching” and frequently frowned upon), changes made by refinements are visible only in the file and module the refinement is used. As the adoption of refinements seems to be questionable, the details of their adjustments are put in a separate “deeper topics” section.
- 2.0 Refinements are introduced as experimental feature with
Module#refine
and top-levelusing
- 2.1
Module#refine
and top-levelusing
are no longer experimental - 2.1
Module#using
introduced to activate refinements only in some particular module - 2.4 Refinements are supported in
Symbol#to_proc
andsend
- 2.4
#refine
can refine modules, too - 2.4
Module.used_modules
- 2.5 Refinements work in string interpolations
- 2.6 Refined methods are achievable with
#public_send
and#respond_to?
, and implicit#to_proc
. - 2.7 Refined methods are achievable with
#method
/#instance_method
- 3.1
Refinement
class representing theself
inside therefine
statement. In particular, new method#import_methods
became available inside#refine
providing some (incomplete) remedy for inability to#include
modules while refining some class. - 3.2
Module#refinements
to introspect which refinements some module defines;- 3.2
Module.used_refinements
to check which refinements are active in the current context; and - 3.2
Refinement#refined_class
to see what class/module they refine. - 3.3
Refinement#refined_class
is renamed toRefinement#target
.
- 3.2
Freezing
Freezing of object makes its state immutable. The important thing about freezing core objects is it allows for many memory optimizations: any instance of the frozen string "test"
can reference the same representation of the string in the memory.
- 2.0 Fixnums, Bignums and Floats are frozen. While number values never were mutable, before Ruby 2.0 it was possible to change additional internal state for them, making it weird:
10.instance_variable_set('@foo', 5) # works in 1.9, "can't modify frozen Fixnum" in 2.0 10.instance_variable_get('@foo') # => 5 in Ruby 1.9
- 2.1 All symbols are frozen.
- 2.1
"string_literal".freeze
is optimized to always return the same object for same literal - 2.2
nil
/true
/false
objects are frozen. - 2.3
String#+@
and#-@
are added to get mutable/frozen strings. - 2.4
Object#clone
:freeze: false
argument to receive unfrozen clone of a frozen object - 2.7 Several core methods like
nil.to_s
andModule.name
return frozen strings - 3.0 Interpolated String literals are no longer frozen when
# frozen-string-literal: true
pragma is used - 3.0
Regexp
andRange
objects are frozen - 3.0
Symbol#name
method that returns a frozen string equivalent of the symbol (Symbol#to_s
returns mutable one, and changing it to be frozen would cause too much incompatibilities) - 3.2
String#dedup
as an alias for-"string"