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 in if SOMETHING_SUPPORTED..., you can just return 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 a true/false check
    if {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 class Thread::Backtrace::Location
  • 2.0 #caller accepts second optional argument n which specify required caller size.
  • 2.2 #throw raises UncaughtThrowError, subclass of ArgumentError when there is no corresponding catch block, instead of ArgumentError.
  • 2.3 #loop: when stopped by a StopIteration exception, returns what the enumerator has returned instead of nil
  • 2.5 #pp debug printing method is available without require '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 returns false by default, can be overrided by respond_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 do AnyClass.include AnyModule without resorting to send(: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 like memoize 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.0 top-level define_method which defines a global function.
  • 2.1 def now returns the symbol of its name instead of nil. 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
    
  • 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 in grep and case:
    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 for UnboundMethod
    • 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 lambda Proc 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 argument lambda: true/false, improving introspection of whether arguments have default values or they are just optional because all proc 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 supports Range argument:
    123.clamp(0..100)
    # one-sided clamp with endless/beginless ranges work too!
    -123.clamp(0..) #=> 0
    123.clamp(..100) #=> 100
    

Numeric

Strings, symbols, regexps, encodings

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, unless keyword_init: true or false was explicitly specified.

Data

  • 3.2 Data: new immutable value object class introduced. It has a stricter and leaner interface than Struct:
    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 time
    Time.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 than Time.parse of the standard library)

Enumerables, collections, and iteration

Numeric iteration

  • 2.1 Numeric#step allows the limit argument to be omitted, producing Enumerator. Keyword arguments to and by 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 by Range#step and Numeric#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 for Range#step for expressiveness: (1..10) % 2 produces ArithmeticSequence 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 and Enumerator::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 pass size argument to Enumerator.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 for reduce(:+); might be redefined in descendants (like Array) 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 with Enumerator#+ and Enumerable#chain, producing Enumerator::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})
    • 3.1 #tally accepts an optional hash to append results to
  • 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 of Enumerable-alike objects.

Range

  • 2.6 Endless range: (1..)
  • 2.6 #=== uses #cover? instead of #include? which means that ranges can be used in case and grep 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 of Enumerable#reverse_each)
  • 3.3 Range#overlap?

Array

Hash

  • 2.0 Introduced convention of #to_h method for explicit conversion to hashes, and added it to Hash, nil, and Struct;
    • 2.1 Array#to_h and Enumerable#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.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 of hash2 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 drop nil 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?, so Set can be used in grep and case:
    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 of set standard library before) has been removed for dependency and performance reasons (it silently depended upon rbtree 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

Filesystem and IO

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 and NoMethodError 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 and Range operations: raise original exception and not “can’t be coerced” or “bad value for range”
  • 2.6 else in exception-handling context without any rescue is prohibited.
  • 2.6 #Integer() and other similar conversion methods now have optional argument exception: true/false, defining whether to raise error on input that can’t be converted or just return nil
  • 2.6 #system: optional argument exception: true/false
  • 2.6 New arguments: receiver: for NameError.new and NoMethodError.new; key: for KeyError.new. It allows user code to construct errors with the same level of detail the language can.
  • 2.6 Exception#full_message: formatting options highlight: and order: 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 calls Warning.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::[] and Warning::[]= 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 category Warning#warn with category: keyword argument; the list of categories is still closed.
  • 3.3 New Warning category: :performance.

Concurrency and parallelism

Thread

Process

Fiber

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

  • 2.6 RubyVM.resolve_feature_path introduced
    • 2.7 …and was renamed to $LOAD_PATH.resolve_feature_path

Binding

Binding object represents the execution context and allows to pass it around.

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.

TracePoint

RubyVM::AbstractSyntaxTree

RubyVM::InstructionSequence

InstructionSequence is an API to interact with Ruby virtual machine bytecode. It is implementation-specific.

ObjectSpace

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-level using
  • 2.1 Module#refine and top-level using 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 and send
  • 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 the self inside the refine 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;

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.
    • The methods are mnemonical to those using Celsius temperature scale, where 0 is freezing point, so any “minus-something” is frozen while “plus-something” is not.
    • 2.5 String#-@ deduplicates frozen strings.
  • 2.4 Object#clone: freeze: false argument to receive unfrozen clone of a frozen object
    • 3.0 freeze: true also works, for consistency.
    • 3.0 freeze: argument is passed to #initialize_clone
  • 2.7 Several core methods like nil.to_s and Module.name return frozen strings
  • 3.0 Interpolated String literals are no longer frozen when # frozen-string-literal: true pragma is used
  • 3.0 Regexp and Range 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"

Appendix: Covered Ruby versions release dates

  • 2.0 — 2013, Feb 24
  • 2.1 — 2013, Dec 25 (the same as every version after this)
  • 2.2 — 2014
  • 2.3 — 2015
  • 2.4 — 2016
  • 2.5 — 2017
  • 2.6 — 2018
  • 2.7 — 2019
  • 3.0 — 2020
  • 3.1 — 2021
  • 3.2 — 2022
  • 3.3 — 2023