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
@@variableis 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
returnto 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_SUPPORTEDat the beginning.
Pattern-matching
- 2.7
Pattern-matchingintroduced 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
inpattern-matching expression repurposed as atrue/falsecheckif {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#deconstructand#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_locationswhich returns an array of frame information objects, in a form of new classThread::Backtrace::Location- 3.2
Thread.each_caller_locationas an efficient method to iterate through part of the call stack.
- 3.2
- 2.0
#calleraccepts second optional argumentnwhich specify required caller size. - 2.2
#throwraisesUncaughtThrowError, subclass ofArgumentErrorwhen there is no corresponding catch block, instead ofArgumentError. - 2.3
#loop: when stopped by aStopIterationexception, returns what the enumerator has returned instead ofnil - 2.5
#ppdebug printing method is available withoutrequire 'pp' - 3.1
#loadallows 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 returnsfalseby default, can be overrided byrespond_to?(:foo, true). - 2.0
#respond_to_missing?,#initialize_clone,#initialize_dupbecame private. - 2.1
#singleton_method - 2.2
#itselfintroduced, 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
#prependintroduced: like#include, but adds prepended module to the beginning of the ancestors chain (also#prependedand#prepend_featureshooks):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_getaccepts 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
#includeand#prependare now public methods, so one can doAnyClass.include AnyModulewithout resorting tosend(:include, ...)(which people did anyway) - 2.3
#deprecate_constant - 2.5 methods for defining methods and accessors (like
#attr_readerand#define_method) became public - 2.6
#method_defined?:inheritargument - 2.7
#const_source_locationallows to query where some constant (including modules and classes) was first defined. - 2.7
#autoload?:inheritargument. - 3.0
#includeand#prependnow 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, :cwork (#attr_readerstarted to return arrays of symbols, and#privateaccepts arrays) - 3.1
Class#subclasses - 3.1
Module#prependbehavior changed to take effect even if the same module is already included. - 3.1
#privateand other visibility methods return their arguments, to allow usage in macros likememoize private def my_method... - 3.2
Class#attached_objectfor singleton classes. - 3.2
Module#const_addedhook 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_nameto 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_methodwhich defines a global function. - 2.1
defnow 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 endModule#define_methodandObject#define_singleton_methodalso 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 ingrepandcase: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*argswouldn’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#inspectwith signature and source code location - 2.7
UnboundMethod#bind_call - 3.0 Arguments forwarding (
...)supportsleading 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.
- 3.4
**nilunpacksinto empty keyword arguments.
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
ArgumentErroris no longer raised when lambdaProcis 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
Proccomposition 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 allprocarguments are. - 3.3 Added a warning that since 3.4,
itwould become a synonym for_1. - 3.4
`it`anonymous argument:[1, 2, 3].map { it * 100 } #=> [100, 200, 300]
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
#clampsupportsRangeargument: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
FixnumandBignumare unified intoInteger - 2.4
Numeric#infinite?and#finite? - 2.4
Integer#digits - 2.4 Rounding methods
Numeric#ceil,Numeric#floor,Numeric#truncate:ndigitsoptional argument. - 2.4
Integer#roundandFloat#round:half:argument - 2.5
Integer#pow:moduloargument - 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 - 3.4
Kernel#FloatandString#to_fallow to omit digits after dot:'1.E2'.to_f #=> 100.0
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#encodeandString#force_encoding(introduced in 1.9) should be preferred
- 2.0
%isymbol array literals shortcut:%i[first_name last_name age] # => [:first_name, :last_name, :age] - 2.0
String#bto set string encoding as ASCII-8BIT (aka “binary”, raw bytes). - 2.1
String#scruband#scrub!to verify and fix invalid byte sequence. - 2.2 Most symbols which are returned by
String#to_symare 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.newaccepts 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#casecmpwhen boolean value is needed (#casecmpreturns-1/0/1):'FOO'.casecmp?('foo') # => true 'Straße'.casecmp?('STRASSE') # => true, Unicode-aware - 2.4
String#concatand#prependaccept multiple arguments - 2.4
String#unpack1as 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 alocateMatchDatainstance, which might make the check more efficient. - 2.4
MatchData: better support for named captures:#named_captures,#values_at - 2.5
String#delete_prefixand#delete_suffix - 2.5
String#grapheme_clustersand#each_grapheme_cluster - 2.5
String#undumpdeserialization 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#splitsupports 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#unpackand#unpack1addedoffset:argument, to unpack data from the middle of a stream. - 3.1
MatchData#matchandMatchData#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. - 3.4
String#append_as_bytes - 3.4
MatchData#bytebeginandMatchData#byteend
Struct
- 2.5 Structs
initializedby 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.newaccepts both positional and keyword arguments by default, unlesskeyword_init: trueorfalsewas 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.atsupports 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#floorand#ceil - 3.1
.new,.at, and.now:in: time_zone_or_offsetparameter 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.newcan parse a string (stricter and more robust thanTime.parseof the standard library) - 3.4
#xmlschemaand#iso8601became core methods
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,#bytesnow return arrays instead of an enumerators (methods for returning enumerators are#each_line,#each_charand so on).IO#lines,#bytes,#charsand#codepointsare deprecated in favor of#each_line,#each_byteand so on.
- 2.0 Binary search introduced in core with
Range#bsearchandArray#bsearch. - 2.3
#digintroduced (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#stepallows the limit argument to be omitted, producingEnumerator. Keyword argumentstoandbyare 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::ArithmeticSequenceis introduced as a type returned byRange#stepandNumeric#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#stepfor expressiveness:(1..10) % 2producesArithmeticSequencewith 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#lazyandEnumerator::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#sizefor on-demand size calculation when possible. The code that creates Enumerator, might passsizeargument toEnumerator.new(value or a callable object) if it can calculate the amount of objects to enumerate.Range#sizeadded, returning non-nilvalue only for integer ranges
- 2.2
Enumerable#slice_afterand#slice_when - 2.2
Enumerable#min,#min_by,#maxand#max_bysupport optional argument to return multiple elements:[1, 6, 7, 2.3, -100].min(3) # => [-100, 1, 2.3] - 2.3
Enumerable#grep_vand#chunk_while - 2.4
Enumerable#sumas 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
Enumeratorchaining 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.produceto 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#tallymethod 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.productintroduced 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 incaseandgrepfor 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? - 3.4
#sizeraisesTypeErrorif the range is not iterable. - 3.4
#stepallows iterating by using+operator for all types:(Time.new(2024, 12, 20)..Time.new(2024, 12, 24)).step(24*60*60).to_a #=> [2024-12-20 00:00:00 +0200, 2024-12-21 00:00:00 +0200, 2024-12-22 00:00:00 +0200, 2024-12-23 00:00:00 +0200, 2024-12-24 00:00:00 +0200]
Array
- 2.0
#shuffleand#sample:random:optional parameter that accepts random number generator, will be called withmaxargument. - 2.3
#bsearch_index - 2.4
#concattakes multiple arguments - 2.4
#pack:buffer:keyword argument to provide target - 2.5
#appendand#prepend - 2.6
#unionand#difference - 2.7
#intersection - 3.1
#intersect? - 3.4
#fetch_values
Hash
- 2.0 Introduced convention of
#to_hmethod for explicit conversion to hashes, and added it toHash,nil, andStruct;- 2.1
Array#to_handEnumerable#to_hwere added. - 2.6
#to_haccepts 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_hashimplicit conversion method, if it has one. - 2.2 Change overriding policy for duplicated key:
{**hash1, **hash2}contains values ofhash2for 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
#compactand#compact!to dropnilvalues - 2.4
#transform_valuesand#transform_values! - 2.5
#transform_keysand#transform_keys! - 2.5
#slice - 2.6
#mergesupports 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} - 3.4
.newaccepts an optionalcapacity:argument - 3.4
#inspectrendering have been changed:p({x: 1, 'foo-bar': 2, "baz" => 3}) # Ruby 3.3: {:x=>1, :"foo-bar"=>2, "baz"=>3} # Ruby 3.4: {x: 1, "foo-bar": 2, "baz" => 3}
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_identityand#compare_by_identity? - 2.5
#===as alias to#include?, soSetcan be used ingrepandcase: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 ofsetstandard library before) has been removed for dependency and performance reasons (it silently depended uponrbtreegem). - 3.0
#joinis 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
Setbecame a built-in class - 3.3
Set#mergeaccepts multiple arguments.
Other collections
- 2.0
ObjectSpace::WeakMapintroduced - 2.3
Thread::Queue#closeis added to notice a termination - 2.7
ObjectSpace::WeakMap#[]=now accepts non-GC-able objects - 3.1
Thread::Queue.newallows initial queue content to be passed - 3.2
Thread::Queue#pop,Thread::SizedQueue#pop, andThread::SizedQueue#pushhavetimeout:argument. - 3.3
ObjectSpace::WeakKeyMapintroduced - 3.3
ObjectSpace::WeakMap#delete - 3.3
Thread::Queue#freezeandThread::SizedQueue#freezeraiseTypeError.
Filesystem and IO
- 2.1
IO#seekimprovements: supportsSEEK_DATAandSEEK_HOLE, and symbolic parameters (:CUR,:END,:SET,:DATA,:HOLE) for 2nd argument. - 2.1
IO#read_nonblockand#write_nonblockaccepts optionalexception: falseto return symbols - 2.2
Dir#fileno - 2.2
File.birthtime,#birthtime, andFile::Stat#birthtime - 2.3
File.mkfifo - 2.3 New
flags/constantsfor 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#preadandIO#pwrite - 2.5
IO#writeaccepts multiple arguments - 2.5
File.openbetter supportsnewline:option - 2.5
File.lutime - 2.5
Dir.childrenand.each_child- 2.6
#childrenand#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: optionallevelto go up the directory tree - 3.1
IO::Bufferlow-level class introduced - 3.2 Support for timeouts for blocking IO via
IO#timeout=. - 3.2 Generic
IO#paththat can be assigned oncreation. - 3.3
Dir.for_fdandDir.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#pathmethod to return the file name that could not be loaded. - 2.1
Exception#causeprovides the previous exception which has been caught at where raising the new exception. - 2.1
Exception#backtrace_locations - 2.3
NameError#receiverstores an object in context of which the error have happened. - 2.3
NameErrorandNoMethodErrorsuggest possible fixes with did_you_mean gem:'test'.szie # NoMethodError: undefined method `szie' for "test":String # Did you mean? size - 2.5
rescue/else/ensureare 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:#receiverand#keymethods - 2.5 New class:
FrozenError - 2.5 Don’t hide coercion errors in
NumericandRangeoperations: raise original exception and not “can’t be coerced” or “bad value for range” - 2.6
elsein exception-handling context without anyrescueis 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.newandNoMethodError.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.limitreader to get the maximum backtrace size set with--backtrace-limitcommand-line option - 3.2
Exception#detailed_messageto separate the original error message and possible contextual additions. - 3.2
SyntaxError#path - 3.4
#backtrace_locationscan be set programmatically onException#set_backtraceandKernel#raise - 3.4 Backtrace formatting adjustments: backtick is replaced with singular quote, and module name added to labels in the call stack.
Warnings
- 2.0
Kernel#warnaccepts multiple args in like#puts. - 2.4
Warningmodule introduced - 2.5
Kernel#warncallsWarning.warninternally - 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:deprecatedor:experimental(or none)- 3.0 User code allowed to specify category of its warnings with
Kernel#warnand intercept the warning categoryWarning#warnwithcategory:keyword argument; the list of categories is still closed.
- 3.0 User code allowed to specify category of its warnings with
- 3.3 New
Warningcategory::performance. - 3.4
.categories
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_interruptto setup handling on exceptions and.pending_interrupt?/#pending_interrupt? - 2.0
#joinand#valuenow raises aThreadErrorif target thread is the current or main thread. - 2.0 Thread-local
#backtrace_locations - 2.3
#nameand#name= - 2.4
.report_on_exception/.report_on_exception=and#report_on_exception/#report_on_exception= - 2.5
#fetchis toThread#[]likeHash#fetchis 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
.getsidfor getting session id (unix only). - 2.1
.argv0returns the original value of$0. - 2.1
.setproctitlesets the process title without affecting$0. - 2.1
.clock_gettimeand.clock_getres - 2.5
Process.last_statusas an alias of$? - 3.1
Process._fork - 3.3
Process.warmup
Fiber
- 2.0
#resumecannot resume a fiber which invokes#transfer. - 2.2
callccis obsolete, andFibershould be used - 2.7
#raise - 3.0 Non-blocking
FiberandFiber::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::SchedulerInterfaceis renamed toFiber::Schedulerfor documetation purposes; - 3.2
#io_selecthook added
- 3.1 New hooks for fiber scheduler:
- 3.0
#backtraceand#backtrace_locations - 3.2 Storage concept:
.[],.[]=,#storage, and#storage= - 3.3
Fiber#kill - 3.4
Fiber::Scheduler#blocking_operation_wait
Ractor
- 3.0
Ractorsintroduced. 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
- 3.4
requireworks in Ractors - 3.4
.main? - 3.4
.[],.[]=, and.store_if_absent
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#bindingraises 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_inforeturns:stateto represent current GC status. - 2.2 Rename
.statentries - 2.7
.compact - 3.0
.auto_compactand.auto_compact= - 3.1 Measuring total time spent in GC:
.measure_total_time,.measure_total_time=,.statoutput updated,.total_timeadded - 3.2
GC.latest_gc_info: addneed_major_gc:key - 3.4
GC.configand ability to disable major GC.
TracePoint
- 2.0
TracePointclass 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_compiledevent (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#bindingreturnsnilforc_call/c_return - 3.2
TracePoint#enablewith a block default to trace the current thread. - 3.3
TracePointsupports:rescueevent.
RubyVM::AbstractSyntaxTree
- 2.6
RubyVM::AbstractSyntaxTreeintroduced - 3.2
.parse:error_tolerant: trueoption for parsing - 3.2
.parse:keep_tokens: trueoption for parsing, allowing access toNode#tokensandNode#all_tokens. - 3.4
Node#locationsandLocation
RubyVM::InstructionSequence
InstructionSequence is an API to interact with Ruby virtual machine bytecode. It is implementation-specific.
- 2.0
.ofto get the instruction sequence from a method or a block. - 2.0
#path,#absolute_path,#label,#base_labeland#first_linenoto retrieve information from where the instruction sequence was defined. - 2.3
#to_binary - 2.3
.load_from_binaryand.load_from_binary_extra_data - 2.5
#each_child,#trace_points
ObjectSpace
- 3.2
ObjectSpace#dump_allallow 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#refineand top-levelusing - 2.1
Module#refineand top-levelusingare no longer experimental - 2.1
Module#usingintroduced to activate refinements only in some particular module - 2.4 Refinements are supported in
Symbol#to_procandsend - 2.4
#refinecan refine modules, too - 2.4
Module.used_modules - 2.5 Refinements work in string interpolations
- 2.6 Refined methods are achievable with
#public_sendand#respond_to?, and implicit#to_proc. - 2.7 Refined methods are achievable with
#method/#instance_method - 3.1
Refinementclass representing theselfinside therefinestatement. In particular, new method#import_methodsbecame available inside#refineproviding some (incomplete) remedy for inability to#includemodules while refining some class. - 3.2
Module#refinementsto introspect which refinements some module defines;- 3.2
Module.used_refinementsto check which refinements are active in the current context; and - 3.2
Refinement#refined_classto see what class/module they refine. - 3.3
Refinement#refined_classis 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".freezeis optimized to always return the same object for same literal - 2.2
nil/true/falseobjects are frozen. - 2.3
String#+@and#-@are added to get mutable/frozen strings. - 2.4
Object#clone:freeze: falseargument to receive unfrozen clone of a frozen object - 2.7 Several core methods like
nil.to_sandModule.namereturn frozen strings - 3.0 Interpolated String literals are no longer frozen when
# frozen-string-literal: truepragma is used - 3.0
RegexpandRangeobjects are frozen - 3.0
Symbol#namemethod that returns a frozen string equivalent of the symbol (Symbol#to_sreturns mutable one, and changing it to be frozen would cause too much incompatibilities) - 3.2
String#dedupas an alias for-"string"