Ruby 2.7

  • Released at: Dec 25, 2019 (NEWS file)
  • Status (as of Dec 25, 2023): 2.7.8 is EOL
  • This document first published: Dec 27, 2019
  • Last change to this document: Dec 25, 2023

Highlights

Ruby 2.7 is a last major release before 3.0¹, so it introduces several important changes, larger in scale than previous releases (and also a bit lean on a “just nice to have” features side). Be prepared!

¹There is a possibility 2.8 will also be released, but in any case, Christmas release of 2020 is promised to be Ruby 3.0.

Language

Pattern matching

Pattern matching is a completely new and experimental feature for structural value checking against patterns, and local variable binding. As it is new and huge, we’ll not try to cover the feature here and just send the reader to the official documentation. Just a small example:

require 'open-uri'
require 'json'

data = URI.open('https://api.github.com/repos/ruby/ruby/pulls').read
       .then { |body| JSON.parse(body, symbolize_names: true) }

data in [{user: {login:}, title:, created_at:}, *] # match array of hashes, with deep matching inside first hash

[login, title, created_at] # matched values bound to local variables
#  => ["zverok", "Add pattern matching documentation", "2019-12-25T18:42:03Z"]
  • Follow-ups:
    • Pattern matching became a stable (non-experimental) feature, and its power expanded signficantly in 3.0;
    • Then, it became even more flexible in 3.1;
    • In 3.2, several core and standard library classes (MatchData, Time, Date, DateTime) became deconstructible.

Ruby 2.7 introduced a lot of changes towards more consistent keyword arguments processing. Fortunately, the official Ruby site has a full description of those changes, with examples, justifications and relationships of features with each other.
Therefore here we’ll just list the changes for the sake of completeness of this changelog.

  • Warnings (would be errors in 3.0) for implicit conversion of the last argument-hash to keyword arguments, and vice versa;
  • Module#ruby2_keywords and Proc#ruby2_keywords methods to mark method and proc as not warning when the last argument is used as keyword one (to provide backward- and forward-compatible way of defining “delegating all” methods);
  • “Forward all arguments” syntax: (...)
  • Non-Symbol keys are allowed in keyword arguments unpacking;
  • **nil syntax in method definition to explicitly mark method that doesn’t accept keywords (and can’t be called with a hash without curly braces);
  • empty hash splat doesn’t pass empty hash as a positional argument.

  • Follow-up: Warnings became errors in 3.0, and some edge cases were fixed.

Numbered block parameters

In block without explicitly specified parameters, variables _1 through _9 can be used to reference parameters.

  • Reason: It is one of the approaches to make short blocks DRY-er and easier to read. E.g. in filenames.each { |f| File.read(f) }, repetition of f and extra syntax needed for it can be considered an unnecessary verbosity, so each { File.read(_1) } could be now used instead.
  • Discussion: The feature was discussed for a long time, syntax and semantics was changed several times on the road to 2.7:
    • Feature #4475 (initial discussion, started 9 years ago, and finished with accepting of @0@9),
    • Misc #15723 (change to _0_9),
    • Bug #16178 (dropping of _0, and changing semantics of _1)
  • Documentation: Proc#Numbered parameters
  • Code:
    # Simplest usage:
    [10, 20, 30].map { _1**2 }
    # => [100, 400, 900]
    
    # Multiple block parameters can be accessed as subsequent numbers
    [10, 20, 30].zip([40, 50, 60], [70, 80, 90]).map { _1 + _2 + _3 }
    # => [120, 150, 180]
    
    # If only _1 is used for multi-argument block, it contains all arguments
    [10, 20, 30].zip([40, 50, 60], [70, 80, 90]).map { _1.join(',') }
    # => ["10,40,70", "20,50,80", "30,60,90"]
    
    # If the block has explicit parameters, numbered one is SyntaxError
    [10, 20, 30].map { |x| _1**2 }
    # SyntaxError ((irb):1: ordinary parameter is defined)
    
    # Outside the block, usage is warned:
    _1 = 'test'
    # warning: `_1' is reserved as numbered parameter
    
    # But after that, _1 references local variable:
    [10].each { p _1 }
    # prints "test"
    
    # Numbered parameters are reflected in Proc's parameters and arity
    p = proc { _1 + _2 }
    l = lambda { _1 + _2 }
    p.parameters
    # => [[:opt, :_1], [:opt, :_2]]
    p.arity
    # => 2
    l.parameters
    # => [[:req, :_1], [:req, :_2]]
    l.arity
    # => 2
    
    # Nested blocks with numbered parameters are not allowed:
    %w[test me].each { _1.each_char { p _1 } }
    # SyntaxError (numbered parameter is already used in outer block here)
    # %w[test me].each { _1.each_char { p _1 } }
    #                    ^~
    
  • Follow-ups:
    • 3.0: Warning on attempt to assign to numbered parameter became errors.
    • 3.3: a warning introduced indicating that in Ruby 3.4, it would become an alternative anonymous block parameter (only one, same as _1). There is no plan to deprecate numbered parameters.

Beginless range

In addition to endless range in Ruby 2.6: (start..), Ruby 2.7 introduces beginless one: (..end).

  • Reason: Array slicing (initial justification for endless ranges) turned out to be not the only usage for semi-open ranges. Another ones, like case/grep, DSLs and constants and Comparable#clamp, can gain from the symmetry of “range without end”/”range without beginning”.
  • Discussion: Feature #14799
  • Documentation: doc/syntax/literals.rdoc#Ranges
  • Code:
    # Usage examples
    %w[a b c][..1] # same as [0..1], not really useful
    
    case creation_time
    when ...1.year.ago then 'ancient'
    when 1.year.ago...1.month.ago then 'old'
    when 1.month.ago...1.day.ago then 'recent'
    when 1.day.ago... then 'new'
    end
    
    users.map(&:age).any?(...18)
    
    # Properties:
    r = ..10
    r.begin # => nil
    r.count # TypeError (can't iterate from NilClass), same with any other Enumerable methods
    r.size  # => Infinity
    
  • Notes:
    • Slightly “a-grammatical” name (“beginless” instead of “beginningless”) is due to the fact that it is a range literally lacking begin property.
    • Unfortunately, parser can not always handle beginless range without the help of the parentheses:
      (1..10).grep ..5
      # ArgumentError (wrong number of arguments (given 0, expected 1))
      # ...because it is in fact parsed as ((1..10).grep()..5), e.g. range from grep results to 5
      

      This may seem an esoteric problem, but it becomes less esoteric in DSLs. For example in RubySpec, one would like to write:

      ruby_version ..'1.9' do
      # some tests for old Ruby
      end
      

      …but the only way is

      ruby_version(..'1.9') do
      # some tests for old Ruby
      end
      

Other syntax changes

  • Comments between .foo calls are now allowed:
    (1..20).select(&:odd?)
           # This was not possible in 2.6
           .map { |x| x**2 }
    # => [1, 9, 25, 49, 81, 121, 169, 225, 289, 361]
    
  • Quotes (if exist) in HERE-documents should be on the same line as document start:
     <<"EOS
     " # This had been warned since 2.4; Now it raises a SyntaxError
     EOS
    
  • Modifier rescue parsing change (multiple assignment now consistent with singular):

    a = raise rescue 1
    # => 1
    a # => 1 in Ruby 2.6 and 2.7
    
    a, b = raise rescue [1, 2]
    # => [1, 2]
    
    # 2.6
    a # => nil
    b # => nil
    # The statement parsed as: (a, b = raise) rescue [1, 2]
    
    # 2.7
    a # => 1
    b # => 2
    # The statement parsed as: a, b = (raise rescue [1, 2])
    

Warnings/deprecations

Some older or accidental features are deprecated on the road to 3.0 and currently produce warnings.

Note that Ruby 2.7 also introduced a way to turn off only some categories of warnings, for example, only deprecation ones.

  • yield in singleton class syntax (was inconsistent with local variables accessibility). Discussion: Feature #15575.
    def foo
      x = 1
      class << Object.new
        p x   # NameError (undefined local variable or method) -- enclosing scope NOT accessible
        yield # calls block passed to foo, implying enclosing scope IS accessible
        # In Ruby 2.7: warning: `yield' in class syntax will not be supported from Ruby 3.0.
      end
    end
    foo { p :ok }
    
  • $; and $, global variables (Perl ancestry: default split and join separators):
    $; = '???'
    # warning: non-nil $; will be deprecated
    'foo???bar'.split
    # warning: $; is set to non-nil value
    # => ["foo", "bar"]
    
    $, = '###'
    # warning: non-nil $, will be deprecated
    %w[foo bar].join
    # warning: $, is set to non-nil value
    "foo###bar"
    
  • Implicit block capturing in proc and lambda:
    def foo
      proc.call # here the block passed to method is implicitly captured by proc
    end
    foo { puts "Hello" }
    # warning: Capturing the given block using Kernel#proc is deprecated; use `&block` instead
    # still prints "Hello"
    
  • Contrastingly, the flip-flop syntax deprecation, introduced in 2.6, is reverted. It turned out that for brevity in text-processing scrips (including one-liners run as ruby -e "print <something>") the feature, however esoteric it may seem, has a justified usage.
    • Discussion: Feature #5400.
    • Documentation: doc/syntax/control_expressions.rdoc#Flip-Flop
    • Code:
      # Imagine we are working with some data file where real data starts with "<<<"" line and end with ">>>>"
      File.each_line('data.txt', chomp: true).filter_map { |ln| ln if ln == '<<<'..ln == '>>>' }
      # => gets lines starting from <<<, and ending with >>>.
      #   The condition "flips on" when the first part is true, and then "flips off" when the second is true
      
      # Flip-flop-less version would be something like this (with the difference: it ignores last >>>):
      lines.drop_while { |ln| ln != '<<<' }.take_while { |ln| ln != '>>>' }
      
      • Note: The BIG difference with “enumerator-based” filtering, which makes flip-flops useful in complicated data processing, is that enumerator’s can’t express multiple “ons and offs” (imagine file has several <<< ... >>> blocks and try to solve the task without flip-flops).

“Safe” and “taint” concepts are deprecated in general

The concepts of marking objects as "tainted" (unsafe, came from the outside) and “untaint” them after the check, is for a long time ignored by most of the libraries. $SAFE constant, trying to limit “unsafe” calls when set to higher values, is considered a fundamentally flawed method and documented so since Ruby 2.0. At the same time, as the “official” security features, subtle bugs in implementation of $SAFE and tainting have caused lot of “vulnerability reports” and added maintenance burden.

self.<private_method>

Calling a private method with a literal self as the receiver is now allowed.

  • Reason:anything.method is always disallowed for private methods” seemed like a simple and unambiguous rule, but produced some ugly edge cases (for example, self.foo = something for private attr_accessor was allowed). It turned out “allow literal self.” makes things much clearer.
  • Discussion: Feature #11297, Feature #16123
  • Documentation: doc/syntax/modules_and_classes.rdoc#Visibility
  • Code:
    class A
      def update
        self.a = 42 # works even in Ruby 2.6, because there was no other way to call private setter
        self.a += 42 # "private method `a' called" in 2.6, works in 2.7
    
        self + 42 # "private method `+' called" in 2.6, works in 2.7
    
        x = self
        x.a = 42 # "private method `a=' called" even in 2.7, not a literal self
      end
    
      private
    
      attr_accessor :a
    
      def +(other)
        puts "+ called"
      end
    end
    

Refinements in #method/#instance_method

  • Discussion: Feature #15373
  • Code:
    module StringExt
      refine String do
        def wrap(what)
          before, after = self.split('|', 2)
          "#{before}#{what}#{after}"
        end
      end
    end
    
    using StringExt
    
    '<<|>>'.method(:wrap)
    # => #<Method: String(#<refinement:String@StringExt>)#wrap(what) refinement_test.rb:3>
    
    %w[test me please].map(&'<<|>>'.method(:wrap))
    # 2.6: undefined method `wrap' for class `String'
    # 2.7: => ["<<test>>", "<<me>>", "<<please>>"]
    

Core classes and modules

Better Method#inspect

Method#inspect now shows method’s arguments and source location (if available).

  • Reason: As there are more code style approaches that operate Method objects, making them better identifiable and informative seemed necessary.
  • Discussion: Feature #14145
  • Documentation: Method#inspect
  • Code:
    p CSV.method(:read)
    # Ruby 2.6:
    #   => #<Method: CSV.read>
    # Ruby 2.7:
    #   => #<Method: CSV.read(path, **options) <...>/lib/ruby/2.7.0/csv.rb:714>
    
    # For methods defined in C, path and param names aren't available, but at least generic signature is:
    [].method(:at)
    # => #<Method: Array#at(_)>
    
    # Convention: unknown param name is displayed as _, param has default value -- as ...
    def m(a, b=nil, *c, d:, e: nil, **rest, &block)
    end
    
    p method(:m)
    #=> #<Method: m(a, b=..., *c, d:, e: ..., **rest, &block) ...skip...>
    
  • Notes:
    • while enhancing Method#inspect, Proc’s string representation also changed for consistency: now object id separated from location by ` ` (for easier copy-pasting). Discussion: Feature #16101
      p(proc {})
      # Ruby 2.6:
      #   => #<Proc:0x000055e4d93f2708@(irb):13>
      # Ruby 2.7:
      #   => #<Proc:0x000055e4d93f2708 (irb):13>
      
    • The same is related to Thread. Discussion: Feature #16412
      p(Thread.new {})
      # Ruby 2.6:
      #   => #<Thread:0x000055be3d72fcd0@(irb):2 run>
      # Ruby 2.7:
      #   => #<Thread:0x0000561efab16560 (irb):2 run>
      

UnboundMethod#bind_call

Binds UnboundMethod to a receiver and calls it. Semantically equivalent to unbound_method.bind(receiver).call(arguments), but doesn’t produce intermediate Method object (which bind does).

  • Reason: The technique of storing unbound methods and binding them later is used in some metaprogramming-heavy code to robustly use “original” implementation. For example:
    MODULE_NAME = Module.instance_method(:name)
    
    class Customer
      def self.name
        "<Customer Model>"
      end
    end
    
    Customer.name # => "<Customer Model>"
    MODULE_NAME.bind_call(Customer) # => "Customer"
    

    In such cases (for example, code reloaders), overhead of producing new Method object on each bind().call is pretty significant, and bind_call allows to avoid it.

  • Discussion: Feature #15955
  • Documentation: UnboundMethod#bind_call

Module

#const_source_location

Returns the location of the first definition of the specified constant.

  • Discussion: Feature #10771
  • Documentation: Module#const_source_location
  • Code:
    # Assuming test.rb:
    class A
      C1 = 1
    end
    
    module M
      C2 = 2
    end
    
    class B < A
      include M
      C3 = 3
    end
    
    class A # continuation of A definition
    end
    
    p B.const_source_location('C3')           # => ["test.rb", 11]
    p B.const_source_location('C2')           # => ["test.rb", 6]
    p B.const_source_location('C1')           # => ["test.rb", 2]
    p B.const_source_location('C4')           # => nil  -- constant is not defined
    
    p B.const_source_location('C2', false)    # => nil  -- don't lookup in ancestors
    
    p Object.const_source_location('B')       # => ["test.rb", 9]
    p Object.const_source_location('A')       # => ["test.rb", 1]  -- note it is first entry, not "continuation"
    
    p B.const_source_location('A')            # => ["test.rb", 1]  -- because Object is in ancestors
    p M.const_source_location('A')            # => ["test.rb", 1]  -- Object is not ancestor, but additionally checked for modules
    
    p Object.const_source_location('A::C1')   # => ["test.rb", 2]  -- nesting is supported
    p Object.const_source_location('String')  # => []  -- constant is defined in C code
    

#autoload?: inherit argument

  • Reason: More granular checking “if something is marked to be autoloaded directly or through ancestry chain” is necessary for advanced code reloaders (requested by author of zeitwerk).
  • Discussion: Feature #15777
  • Documentation: Module#autoload?
  • Code:
    class Parent
      autoload :Feature1, 'feature1.rb'
    end
    
    module Mixin
      autoload :Feature2, 'feature2.rb'
    end
    
    class Child < Parent
      include Mixin
    end
    
    Child.autoload?(:Feature1)        # => "feature1.rb"
    Child.autoload?(:Feature1, false) # => nil
    Child.autoload?(:Feature2)        # => "feature2.rb"
    Child.autoload?(:Feature2, false) # => nil
    

Comparable#clamp with Range

Comparable#clamp now can accept Range as its only argument, including beginless and endless ranges.

  • Reason: First, ranges can be seen as more “natural” to specify a range of acceptable values. Second, with introduction of beginless and endless ranges, #clamp now can be used for one-sided value limitation, too.
  • Discussion: Feature #14784
  • Documentation: Comparable#clamp
  • Code:
    123.clamp(0..100) # => 100
    -20.clamp(0..100) # => 0
    15.clamp(0..100) # => 15
    
    # With semi-open ranges:
    123.clamp(150..) # => 150
    123.clamp(..120) # => 120
    
    # Range with excluding end is not allowed
    123.clamp(0...150) # ArgumentError (cannot clamp with an exclusive range)
    
    # Old two-argument form still works:
    123.clamp(0, 150)
    # => 123
    

Integer[] with range

Allows to get several bits at once.

  • Discussion: Feature #8842
  • Documentation: Integer#[]
  • Code:
    #    4---0
    #    v   v
    0b10101001[0..4] # => 9
    0b10101001[0..4].to_s(2)
    # => "1001"
    

Complex#<=>

Complex#<=>(other) now returns nil if the number has imaginary part, and behaves like Numeric#<=> if it does not.

  • Reason: Method #<=> was explicitly undefined in Complex to underline the fact that linear order of complex numbers can’t be established, but it was inconsistent with most of the other objects implementations (which return nil for impossible/incompatible comparison instead of raising)
  • Discussion: Bug #15857
  • Documentation: Complex#<=>
  • Code:
    1 + 2i <=> 1 # => nil
    1 + 2i <=> 1 + 2i # => nil, even if numbers are equal
    1 + 0i <=> 2 # => -1
    

Strings, symbols and regexps

  • Unicode version: 12.1
  • New encodigs: CESU-8

Core methods returning frozen strings

Several core methods now return frozen, deduplicated String instead of generating it every time the string is requested.

  • Reason: Avoiding allocations of new strings for each #to_s of primitive objects can save dramatic amounts of memory.
  • Discussion: Feature #16150
  • Affected methods: NilClass#to_s, TrueClass#to_s, FalseClass#to_s, Module#name
  • Code:
    # Ruby 2.6
    true.to_s.frozen? # => false
    3.times.map { true.to_s.object_id }
    # => [47224710953060, 47224710953040, 47224710953000] -- every time new object
    
    # Ruby 2.7
    true.to_s.frozen? # => true
    3.times.map { true.to_s.object_id }
    # => [180, 180, 180] -- frozen special string
    
  • Notes:
    • Change introduces incompatibility for the code looking like:
      value = true
      # ...
      buffer = value.to_s
      buffer << ' -- received'
      # Ruby 2.6: "true -- received"
      # Ruby 2.7: FrozenError (can't modify frozen String: "true")
      
    • The same change was proposed for Symbol#to_s (and could’ve been a dramatic improvement in some kinds of code), but the change turned out to be too disruptive.
  • Follow-up: Instead of freezin Symbol#to_s, new method Symbol#name returning frozen string was introduced in 3.0.

Symbol#start_with? and #end_with?

  • Reason: Symbol was once thought as “just immutable names” with any of “string-y” operations not making sense for them, but as Symbol#match was always present, and Symbol#match? implemented in 2.4, it turns out that other content inspection methods are also useful.
  • Discussion: Feature #16348
  • Documentation: Symbol#end_with?, Symbol#start_with?
  • Code:
    :table_name.end_with?('name', 'value')
    # => true
    :table_name.start_with?('table', 'index')
    # => true
    
    # Somewhat confusingly, Symbol arguments are not supported
    :table_name.end_with?(:name, 'value')
    # TypeError (no implicit conversion of Symbol into String)
    

Time

#floor and #ceil

Rounds Time’s nanoseconds down or up to a specified number of digits (0 by default, e.g. round to whole seconds).

  • Reason: Rounding of nanoseconds important in a test code, when comparing Time instances from a different sources (stored in DB, passed through third-party libraries, etc.). Having better control on truncation comparing to Time#round (which existed since 1.9.2)
  • Discussion: Feature #15653 (floor, Japanese), Feature #15772 (ceil)
  • Documentation: Time#floor, Time#ceil
  • Code:
    t = Time.utc(2019, 12, 24, 5, 43, 25.8765432r)
    t.floor     # => 2019-12-24 05:43:25 UTC
    t.floor(2)  # => 2019-12-24 05:43:25.87 UTC
    
    t.ceil      # => 2019-12-24 05:43:26 UTC
    t.ceil(2)   # => 2019-12-24 05:43:25.88 UTC
    

#inspect includes subseconds

  • Reason: Losing subseconds in #inspect always made debugging and testing harder, producing test failures like “Expected: 2019-12-21 16:11:08 +0200, got 2019-12-21 16:11:08 +0200” (which are visually the same, but one of them, probably going through some serialization or DB storage, has different value for subseconds).
  • Discussion: Feature #15958
  • Documentation: Time#inspect
  • Code:
    t = Time.utc(2019, 12, 24, 5, 43, 25.8765432r)
    p t
    # Ruby 2.6: prints "2019-12-24 05:43:25 UTC"
    # Ruby 2.7: prints "2019-12-24 05:43:25.8765432 UTC"
    puts t
    # always prints "2019-12-24 05:43:25 UTC"
    
    # Note that sometimes representation falls back to Rational fractions:
    t2 = Time.utc(2019,12,31, 23,59,59) + 1.4
    p t2
    # => 2020-01-01 00:00:00 900719925474099/2251799813685248 UTC
    # That's when subseconds can't be represented as 9-digit whole number:
    (t.subsec * 10**9).to_f
    # => 876543200.0
    (t2.subsec * 10**9).to_f
    # => 399999999.99999994
    

Enumerables and collections

Enumerable#filter_map

Transforms elements of enumerable with provided block, and drops falsy results, in one pass.

  • Reason: Filter suitable elements, then process them somehow is a common flow of sequence processing, yet with filter { ... }.map { ... } additional intermediate Array is produced, which is not always desirable. Also, some processing can indicate “can’t be processed” by returning false or nil, which requires code like map { ... }.compact (to drop nils) or map { ... }.select(:itself) (to drop all falsy values).
  • Discussion: Feature #15323
  • Documentation: Enumerable#filter_map
  • Code:
    (1..10).filter_map { |i| i**2 if i.even? } # => [4, 16, 36, 64, 100]
    
    # imagine method constantize() returning false if string can't be converted to
    # a proper constant name
    constant_names = %w[foo 123 _ bar baz/test].filter_map { |str| constantize(str) } # => ['Foo', 'Bar']
    
    # Without block, returns Enumerator:
    %w[foo bar baz test].filter_map
                        .with_index { |str, i| str.capitalize if i.even? }
    # => ["Foo", "Baz"]
    

Enumerable#tally

Counts unique objects in the enumerable and returns hash of {object => count}.

  • Discussion: Feature #11076
  • Documentation: Enumerable#tally
  • Code:
    %w[Ruby Python Ruby Perl Python Ruby].tally
    # => {"Ruby"=>3, "Python"=>2, "Perl"=>1}
    
  • Notes:
    • #tally follows #to_h intuitions (and uses it underneath): objects are considered same if they have the same #hash; orders of keys corresponds to order of appearance in sequence; first object in sequence becomes the key
    • Additional block argument, or, alternatively, additional method tally_by(&block) was proposed in the same ticket to allow code like
      (1..10).tally_by(&:even?) # => {true => 5, false => 5}
      

      …but was not accepted yet.

  • Follow-up: In Ruby 3.1, #tally(hash) was introduced to accumulate statistics from several collections into a single hash.

Enumerator.produce

Produces infinite enumerator by calling provided block and passing its result to subsequent block call.

  • Reason: .produce allows to convert any while-alike or loop-alike loops into enumerators, making possible to work with them in a Ruby-idiomatic Enumerable style.
  • Discussion: Feature #14781
  • Documentation: Enumerator.produce
  • Code:
    require 'date'
    # Before: while cycle to search next tuesday:
    d = Date.today
    while !d.tuesday
      d += 1
    end
    # After: enumerator:
    Enumerator.produce(Date.today, &:succ) # => enumerator of infinitely increasing dates
              .find(&:tuesday?)
    
    require 'strscan'
    PATTERN = %r{\d+|[-/+*]}
    scanner = StringScanner.new('7+38/6')
    # Before: while cycle to implement simple lexer:
    result =
    result << scanner.scan(PATTERN) while !scanner.eos?
    # After: achieving the same with enumerator (which can be passed to other methods to process):
    Enumerator.produce { scanner.scan(PATTERN) }.slice_after { scanner.eos? }.first
    # => ["7", "+", "38", "/", "6"]
    
    # Raising StopIteration allows to stop the iteration:
    ancestors = Enumerator.produce(node) { |prev| prev.parent or raise StopIteration }
    # => enumerator
    enclosing_section = ancestors.find { |n| n.type == :section } # => :section node or nil
    
    # If the initial value is passed, it is an argument to first block call, and yielded as a first
    # value of enumerator
    Enumerator.produce(1) { |prev| p "PREVIOUS: #{prev}"; prev + 1 }.take(3)
    # "PREVIOUS: 1"
    # "PREVIOUS: 2"
    #  => [1, 2, 3]
    
    # If the initial value is not passed, first block call receives `nil`, and the block's first result
    # is yielded as a first value of enumerator
    Enumerator.produce { |prev| p "PREVIOUS: #{prev.inspect}"; (prev || 0) + 1 }.take(3)
    # "PREVIOUS: nil"
    # "PREVIOUS: 1"
    # "PREVIOUS: 2"
    #  => [1, 2, 3]
    

Enumerator::Lazy#eager

Converts lazy enumerator back to eager one.

  • Reason: When working with large data sequences, lazy enumerators are useful tools to not produce intermediate array on each step. But some data consuming methods expect to receive enumerators that would really return data from methods like take_while and not just add them to pipeline.
  • Discussion: Feature #15901
  • Documentation: Enumerator::Lazy#eager
  • Code:
    # Imagine we read very large data file:
    lines =
      File.open('data.csv')
          .each_line
          .lazy
          .map { |ln| ln.sub(/\#.+$/, '').strip } # remove "comments"
          .reject(&:empty?)  # drop empty lines
    p lines
    # => #<Enumerator::Lazy: ....>
    
    # Now, we want to consume just "headers" from this CSV, and pass the rest of enumerator
    # into the other methods.
    
    # This code:
    headers = lines.take_while { |ln| ln.start_with?('$$$') }
    # ...will just produce another lazy enumerator, with `take_while` chained to pipeline.
    
    # Now, this:
    lines = lines.eager # makes the enumerator eager, but (unlike `#force`) doesn't consume it yet
    p lines
    # => #<Enumerator: #<Enumerator::Lazy: ...>:each>
    
    # consumes only several first lines and returns array of headers
    headers = lines.take_while { |ln| ln.start_with?('$$$') }
    # => [array, of, header, lines]
    
    # now we can pass `lines` to methods that expect take_while/take and other similar methods to
    # consume enumerator partially and return arrays
    

Enumerator::Yielder#to_proc

  • Reason: When constructing the enumerator, value yielding is frequently delegated to other methods, which accept blocks. Before the change, it was inconvenient to delegate.
  • Discussion: Feature #15618
  • Documentation: Enumerator::Yielder#to_proc
  • Code:
    # Construct a enumerator which will pass all lines from all files from some folder:
    
    # Before the change:
    all_lines = Enumerator.new { |y| # y is Yielder object here
      Dir.glob("*.rb") { |file|
        File.open(file) { |f| f.each_line { |ln| y << ln } }
      }
    }
    
    # After the change:
    all_lines = Enumerator.new { |y| # y is Yielder object here
      Dir.glob("*.rb") { |file|
        File.open(file) { |f| f.each_line(&y) }
      }
    }
    

Array#intersection

Like Array#union and #difference, added in 2.6 as a explicitly-named and accepting multiple arguments alternatives for #| and #-, the new method is alternative for #&.

  • Discussion: Feature #16155
  • Documentation: Array#intersection
  • Code:
    ['Ruby', 'Python', 'Perl'].intersection(['Ruby', 'Diamond', 'Perl'], ['Ruby', 'Nikole', 'Kate']) # => ["Ruby"]
    
    ['Ruby', 'Python', 'Perl'].intersection # => ["Ruby", "Python", "Perl"]
    
  • Follow-up: Ruby 3.1 introduced Array#intersect? predicate.

ObjectSpace::WeakMap#[]= now accepts non-GC-able objects

  • Reason: ObjectSpace::WeakMap (the Hash variety that doesn’t hold its contents from being garbage-collected) is mostly thought, as the name implies, as an “internal” thing. But turns out it can have some legitimate usages in a regular code, for example, implementing flexible caching (cache which “auto-cleans” on garbage collection). But before this change, keys for WeakMap wasn’t allowed to be non-GC-able (for example, numbers and symbols), which prohibits some interesting usages.
  • Discussion: Feature #16035
  • Documentation: WeakMap#[]=
  • Code:
    map = ObjectSpace::WeakMap.new
    map[1] = Object.new
    # Ruby 2.6: ArgumentError (cannot define finalizer for Integer)
    # Ruby 2.7: Writes the map successfully
    map[1]
    # => #<Object:0x0000561ea70ec3d0>
    GC.start
    map[1]
    # => nil  -- value successfully collected, even if key was not GC-able
    
  • Follow-ups: 3.3: A new class ObjectSpace::WeakKeyMap introduced, more suitable for common use-cases of a “weak mapping.” It only has garbage-collectable keys.

Fiber#raise

Raises exception inside the resumed Fiber.

  • Reason: Ability to raise an exception inside Fiber makes control passing abilities more feature-complete.
  • Discussion: Feature #10344
  • Documentation: Fiber#raise
  • Code:
    f = Fiber.new {
      Enumerator.produce { Fiber.yield } # Infinite yielding enumerator, breaks on StopIteration
                .to_a.join(', ')
    }
    f.resume
    f.resume 1
    f.resume 2
    f.resume 3
    f.raise StopIteration # => "1, 2, 3"
    
  • Notes: Fiber#raise has the same call sequence as Kernel#raise and can be called without any arguments (RuntimeError is empty message is raised), with string (RuntimeError with provided message is raised), exception class and (optional) message, or instance of the exception.

Range

#=== for String

In 2.6, Range#=== was changed to use #cover? underneath, but not for String. This was fixed.

  • Discussion: Bug #15449
  • Documentation: Range#===
  • Code:
    case '2.6.5'
    when '2.4'..'2.7'
      'matches'
    else
      'nope :('
    end
    # => "nope :(" in 2.6, "matches" in 2.7
    

#minmax implementation change

Range#minmax switched to returning Range#end instead of iterating through Range to get maximum value.

  • Reason: Range#minmax was previously implemented in Enumerable, giving some inconsistencies with separate #min and #max in edge cases like:
    (1..).max #=> RangeError (cannot get the maximum of endless range)
    (1..).minmax #=> Runs forever, trying to iterate while it is not exhausted
    ("a".."aa").max #=> "aa"
    ("a".."aa").minmax #=> ["a","z"] -- iteration through range goes till "aa", but then "aa" < "z", so "z" is maximum
    
  • Discussion: Feature #15807
  • Documentation: Range#minmax
  • Code:
    (1..).minmax # => RangeError (cannot get the maximum of endless range)
    (1..Float::INFINITY).minmax # => [1, Infinity]
    ("a".."aa").minmax # => ["a", "aa"]
    
  • Note: As can be seen in String example, sometimes #minmax (as well as #max) can yield unexpected result (the value which is in fact not the maximum of all Range contents). This is true for value types with ambiguous order definition (“zz” is between “a” and “aa” in enumeration, yet still larger than “aa”).

Filesystem and IO

IO#set_encoding_by_bom

Auto-sets encoding to UTF-8 if byte-order mark is present in the stream.

  • Discussion: Feature #15210
  • Documentation: IO#set_encoding_by_bom
  • Code:
    File.write("tmp/bom.txt", "\u{FEFF}мама")
    ios = File.open("tmp/bom.txt", "rb")
    ios.binmode? # => true
    ios.external_encoding # => #<Encoding:ASCII-8BIT>
    ios.set_encoding_by_bom # => #<Encoding:UTF-8>
    ios.external_encoding # => #<Encoding:UTF-8>
    ios.read # => "мама"
    
    File.write("tmp/nobom.txt", "мама")
    ios = File.open("tmp/nobom.txt", "rb")
    ios.set_encoding_by_bom # => nil
    ios.external_encoding # => #<Encoding:ASCII-8BIT>
    ios.read # => "\xD0\xBC\xD0\xB0\xD0\xBC\xD0\xB0"
    
    # The method raises in non-binary-mode streams, or if encoding already set:
    File.open("tmp/bom.txt", "r").set_encoding_by_bom
    # ArgumentError (ASCII incompatible encoding needs binmode)
    File.open("tmp/bom.txt", "rb", encoding: 'Windows-1251').set_encoding_by_bom
    # ArgumentError (encoding is set to Windows-1251 already)
    

Dir.glob and Dir.[] not allow \0-separated patterns

  • Discussion: Feature #14643
  • Documentation: Dir.glob
  • Code:
    Dir.glob("*.rb\0*.md")
    # 2.6:
    #   warning: use glob patterns list instead of nul-separated patterns
    #   => ["2.5.md", "History.md", "README.md" ...
    # 2.7
    #   ArgumentError (nul-separated glob pattern is deprecated)
    
    # Proper alternative, works in 2.7 and earlier versions:
    Dir.glob(["*.rb", "*.md"])
    # => ["2.5.md", "History.md", "README.md" ...
    

File.extname returns a "." string at a name ending with a dot.

  • Reason: It is argued that File.basename(str, '.*') + File.extname(str) should always reconstruct the full name, but it was not the case for names like image.
  • Discussion: Bug #15267
  • Documentation: File.extname
  • Code:
    filename = "image."
    [File.basename(filename, ".*"), File.extname(filename)]
    # 2.6: => ["image", ""]   -- the dot is lost
    # 2.7: => ["image", "."]
    

Exceptions

FrozenError: receiver argument

In 2.6 several exception class constructors were enhanced so the user code could create them providing context, now FrozenError also got this functionality.

  • Discussion: Feature #15751
  • Documentation: FrozenError#new
  • Code:
    class AlwaysFrozenHash < Hash
      # ...
      def update!(*)
        raise FrozenError.new("I am frozen!", receiver: self)
      end
    end
    
  • Notice: <Exception>.new syntax is the only way to pass new arguments, this would not work:
    raise FrozenError, "I am frozen!", receiver: self
    

Interpreter internals

$LOAD_PATH.resolve_feature_path

resolve_feature_path was introduced in Ruby 2.6 as RubyVM method, now it is singleton method of $LOAD_PATH global variable.

  • Reason: It was argued that RubyVM should contain code specific for particular Ruby implementation (e.g. it can be different between CRuby/JRuby/TruffleRuby/etc.), while resolve_feature_path is a generic feature that should behave consistently between Ruby implementations.
  • Discussion: Feature #15903
  • Documentation: As it turns, documenting global’s singleton method is not easy. So it is just mentioned at doc/globals.rdoc
  • Code:
    $LOAD_PATH.resolve_feature_path('net/http')
    # => [:rb, "/home/zverok/.rvm/rubies/ruby-head/lib/ruby/2.7.0/net/http.rb"]
    
  • Notes: As method was just moved, for details of resolve_feature_path behavior, see 2.6 changelog’s entry (with the exception for what described in the next section)

resolve_feature_path behavior for loaded features fixed

In 2.6, resolve_feature_path returned false instead of the path for already loaded libraries. That was fixed.

  • Discussion: Feature #15230
  • Documentation:(see above)
  • Code:
    # Ruby 2.6:
    RubyVM.resolve_feature_path('net/http')
    # => [:rb, "<...>/lib/ruby/2.6.0/net/http.rb"]
    require 'net/http'
    RubyVM.resolve_feature_path('net/http')
    # => [:rb, false]
    
    # Ruby 2.7:
    $LOAD_PATH.resolve_feature_path('net/http')
    # => [:rb, "<...>/lib/ruby/2.7.0/net/http.rb"]
    require 'net/http'
    $LOAD_PATH.resolve_feature_path('net/http')
    # => [:rb, "<...>/lib/ruby/2.7.0/net/http.rb"]
    
  • Follow-up: In Ruby 3.1, the behavior for not found files was adjusted too (to return nil instead of raising).

GC.compact

Ruby 2.7 ships with an improved GC, which allows to manually defragment memory.

  • Reason: After some time of application running, creating objects and garbage collecting them, the memory becomes “fragmented”: there are a large holes of unused memory between actual living objects. The new methods meant to be called between, say, spanning of the new processes/workers, potentially making current process using less memory.
  • Discussion: Feature #15626
  • Documentation: GC.compact
  • Note: This changelog author’s understanding of GC and compacting is far from perfect, so the explanations are sparse. Unfortunately, the new feature is not thoroughly documented yet, so the best guess for understanding the change is reading “discussion” link above. The PRs (to the changelog and/or to the Ruby’s main documentation) are welcome.

Warning::[] and ::[]=

Allows to emit/suppress separate categories of warnings.

  • Reason: 2.7 introduced a lot of new deprecations (especially around keyword arguments, there can easily be thousands), and one “really experimental” feature (pattern matching), which emits warning about its experimental status on every use. To make working with older code, or experimenting with new features, less tiresome, the ability to turn warnings on and off per category was introduced.
  • Discussion: Feature #16345 (deprecated warnings), Feature #16420 (experimental warnings)
  • Documentation: Warning::[], Warning::[]=
  • Code:
    {a: 1} in {a:}
    # warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
    Warning[:experimental] = false
    {a: 1} in {a:}
    # ...no warning issued...
    
    def old_method(hash, **kwargs)
    end
    
    old_method(foo: 'bar')
    # warning: Passing the keyword argument as the last hash parameter is deprecated
    Warning[:deprecated] = false
    old_method(foo: 'bar')
    # ...no warning...
    
    # The current settings can be inspected:
    Warning[:deprecated] # => false
    # ...and changed back:
    Warning[:deprecated] = true
    old_method(foo: 'bar')
    # warning: Passing the keyword argument as the last hash parameter is deprecated
    
  • Notes:
    • The only existing categories currently are :deprecated (covers all deprecations) and :experimental (as of 2.7, covers only pattern matching)
    • Note that turning off :deprecated warning will also mute the warning of features which was deprecated explicitly in your code, for example with Module#deprecate_constant
      class HTTP
        NOT_FOUND = Exception.new
        deprecate_constant :NOT_FOUND
      end
      HTTP::NOT_FOUND
      # warning: constant HTTP::NOT_FOUND is deprecated
      Warning[:deprecated] = false
      HTTP::NOT_FOUND
      # ...no warning issued...
      
    • Another way to turn on and off separate categories of warnings is passing -W:(no-)<category> flag to ruby interpreter, e.g. -W:no-experimental means “no warnings when using experimental features”.
  • Follow-ups:
    • 3.0: user code is allowed to specify warning category, and intercept it;
    • 3.3: added new warning category: :performance.

Standard library

  • Date supports new Japanese era in parsing and rendering dates (generic Date.parse and .jisx0301/#jisx0301). Discussion: Feature #15742
  • DelegateClass() accepts a block to define delegates behavior on-the-fly.
  • Pathname.glob passes third argument, if provided, to Dir.glob, allowing to specify base: for globbing.
  • Pathname() method doesn’t duplicates argument, if it was already a Pathname
  • OptionParser now uses “Did you mean?” feature. Discussion: Feature #16256.
    require 'optparse'
    
    OptionParser.new do |opts|
      opts.on('-t', '--task NAME')
    end.parse!(%w[--tsak build])
    # OptionParser::InvalidOption (invalid option: --tsak)
    # Did you mean?  task
    

Large IRB update

IRB, Ruby’s default console, received its biggest update in years. Now it supports multiline editing, syntax highlighting of input and (some) output, auto-indentation and other modern console behavior. Small demonstration screenshot:

Network and web

  • Net::HTTP#start: new optional keyword parameter ipaddr:, and #ipaddr= setter allows to set the address for the connection manually. Discussion: Feature #5180.
  • open-uri library: 2 versions after the safer alias was added, using Kernel#open finally became deprecated, and URI.open became the main library’s interface. Discussion: Misc #15893
    require 'open-uri'
    open('https://ruby-lang.org')
    # warning: calling URI.open via Kernel#open is deprecated, call URI.open directly or use URI#open
    URI.open('https://ruby-lang.org')
    # => ok
    
  • Net::FTP#features to check available features, and Net::FTP#option to enable/disable each of them. Discussion: Feature #15964.
    ftp = Net::FTP.new('speedtest.tele2.net') # TELE2's open FTP for speed tests
    ftp.features
    # => ["EPRT", "EPSV", "MDTM", "PASV", "REST STREAM", "SIZE", "TVFS"]
    
  • Net::IMAP now has Server Name Indication (SNI) support. Discussion: Feature #15594

Large updated libraries

Standard library contents change

New libraries

  • Reline is a newly introduced readline-compatible pure Ruby line editing library. It is behind the new IRB’s magic.

Libraries promoted to default gems

stdgems.org project has a nice explanations of default and bundled gems concepts, as well as a list of currently gemified libraries.

“For the rest of us” this means libraries development extracted into separate GitHub repositories, and they are just packaged with main Ruby before release. It means you can do issue/PR to any of them independently, without going through more tough development process of the core Ruby.

Libraries extracted in 2.7:

Libraries excluded from the standard library

Unsupported and lesser used libraries removed from the standard library, and now can be installed as a separate gems.

Follow-up: 34 (!) more libraries gemified in 3.0, and 3 more just dropped from the standard library (including infamous WEBrick).