Ruby 2.4

  • Released at: Dec 25, 2016 (NEWS file)
  • Status (as of Oct 14, 2019): EOL soon, latest is 2.4.9
  • Last change to this document: Oct 14, 2019

Highlights

Language

Multiple assignment allowed in conditional expression

  • Reason: It is not what typically considered good style, but multiple assignment in conditions is now consistent with outside of condition behavior.
  • Discussion: Feature #10617
  • Documentation:
  • Code:
    points = []
    if (x, y = points.first) # in Ruby <2.4, will raise SyntaxError: multiple assignment in conditional
      p [x, y]
    end
    
  • Note: Be aware that condition considered false if the whole right hand of assignment is false/nil, not the first assigned variable:
    points = [[]]
    if (x, y = points.first) # x is nil, y is nil, but condition is [], which is truthy
      p [x, y] # will print this
    end
    

Toplevel return

return statement at the top level of any .rb file stops further execution of this file.

  • Reason: Useful for code that depends on platform, presence of third-party libraries and so on; return unless some_condition at the beginning of the file will allow writing the rest of the code in assumption that necessary condition satisfied.
  • Discussion: Feature #4840
  • Documentation:
  • Code:
    # Some nokogiri_patch.rb
    
    # In Ruby < 2.4:
    if defined? Nokogiri
      # the rest of the code should all be nested in the unless
    end
    
    # Ruby 2.4
    return unless defined? Nokogiri
    
    # The rest of the code can be written at the top level, now.
    

Refinements improvements

NB: Some of those changes are language-level, some are just method changes, but all a related to refinements.

Refinements are supported in Symbol#to_proc and send

  • Discussions: Feature #9451, Feature #11476
  • Code:
    module Tests
      refine Numeric do
        def normalize100
          clamp(0, 100)
        end
      end
    end
    
    using Tests
    
    p [1, 700, 132].map(&:normalize100)
    # Ruby 2.3: undefined method `normalize100' for 1
    # Ruby 2.4: => [1, 100, 100]
    
    p 123.send(:normalize100)
    # Ruby 2.3: undefined method `normalize100' for 1
    # Ruby 2.4: => 100
    

refine can refine modules, too

Previously, only classes could’ve been refined.

  • Discussion: Feature #12534
  • Documentation: Module#refine
  • Code:
    module Tests
      refine Enumerable do # in 2.3: wrong argument type Module (expected Class)
        def tally
          each_with_object(Hash.new(0)) { |el, counter| counter[el] += 1}
        end
      end
    end
    
    using Tests
    p [1, 3, 1, 2, 1, 3].tally
    # => {1=>3, 3=>2, 2=>1}
    

Module.used_modules

Returns an array of all modules used in the current scope.

  • Discussion: Feature #7418
  • Documentation: Module.used_modules
  • Code:
    module First
      refine Enumerable do
      end
    end
    
    module Second
      refine Object do
      end
    end
    
    module NoRefinements
    end
    
    p Module.used_modules # => []
    
    using First
    using Second
    using NoRefinements
    
    p Module.used_modules # => [First, Second] -- note NoRefinements absence
    

    Note that order of modules in array returned is not guaranteed.

Core

Warning module

New single-method module was introduced, meant to be overriden in order to control warnings issued by Ruby.

  • Reason:
  • Discussion: Feature #12299
  • Documentation: (introduced in 2.4, but documented in 2.5) Warning
  • Code:
    def Warning.warn(msg)
      puts ".warn called with: #{msg.inspect}"
    end
    
    X = 1
    X = 2
    # Prints:
    #   .warn called with: "<location>: warning: already initialized constant X\n"
    #   .warn called with: "<location>: warning: previous definition of X was here\n"
    
  • Follow-up: Surprisingly as at may be, Kernel#warn haven’t been changed to call Warning.warn in 2.4, but it was fixed in 2.5:
    def Warning.warn(msg)
      puts ".warn called with: #{msg.inspect}"
    end
    
    warn 'foo', 'bar'
    # Ruby 2.4 prints:
    #  foo
    #  bar
    # Ruby 2.5 prints:
    #  .warn called with: "foo\nbar\n"
    

Object#clone(freeze: false)

Allows to receive unfrozen copy of a frozen object.

  • Reason: Previously, there were no way to receive an unfrozen copy of the frozen object, including its singleton class: .dup returns unfrozen object, but doesn’t copies the singleton class, while .clone copies both (singleton class & frozen state).
  • Discussion: Feature #12300
  • Documentation: Object#clone
  • Code:
    h = {breed: 'Dog', name: 'Rex'}
    class << h
      def bark
        puts "Bark! Bark!"
      end
    end
    h.freeze
    h.bark    # "Bark! Bark!"
    
    d = h.dup
    d.frozen? # => false
    d.bark    # NoMethodError: undefined method `bark'
    
    h2 = h.clone(freeze: false)
    h2[:age] = 8
    h2.bark   # "Bark! Bark!"
    
  • Note: Surprisingly enough, unfrozen_object.clone(freeze: true) doesn’t make object frozen. See discussion.

Comparable#clamp

Method to limit any comparable value to minmax range

  • Discussion: Feature #10594
  • Documentation: Comparable#clamp
  • Code:
    123.clamp(0, 20)  # => 20
    -123.clamp(0, 20) # => 0
    18.clamp(0, 20)   # => 18
    
  • Follow-up: For Ruby 2.7, clamp with a range was accepted, which, especially when combined with endless (2.6) and beginless (2.7) ranges, will allow to use more powerful and idiomatic code:
    123.clamp(0..20)  # => 20
    123.clamp(..20)   # => 20
    -123.clamp(0..)   # => 0
    

Numerics

Fixnum and Bignum are unified into Integer

Historically, Ruby had two subclasses of Integer: Fixnum for numbers that fit into machine word, and Bignum for larger numbers. Since Ruby 2.4, there is only one Integer; Fixnum and Bignum are defined as (deprecated) constants synonymous to it.

  • Reason: Fixnum/Bignum separation always been an implementation detail, which led to confusion and sudden bugs, now this detail is hidden by interpreter.
  • Discussion: Feature #12005
  • Documentation: Integer
  • Code:
    # Before 2.4.0
    10.class        # => Fixnum
    (10**100).class # => Bignum
    
    # 2.4+
    10.class        # => Integer
    (10**100).class # => Integer
    Fixnum
    # warning: constant ::Fixnum is deprecated
    # => Integer
    

Numeric#finite? and #infinite?

  • Reason: The methods were present in Float and BigDecimal, but not in other numeric classes, which made it harder to write code uniformly processing numbers which may be integer/float/infinite.
  • Discussion: Feature #12039
  • Documentation: Numeric#infinite?, Numeric#finite?
  • Code:
    1.infinite?  # => nil
    1.finite?    # => true
    
  • Note: Notice that infinite? returns nil/-1/1 (always nil for integers), not true/false as most of other predicate methods. While unusual, it is convenient for checking both for infinity and its sign (+Infinity/-Infinity), and can be treated effectively as true/false in boolean context.

Integer#digits

Returns an array of digits of the number.

  • Reason: Useful for calculating checksums.
  • Discussion: Feature #12447
  • Documentation: Integer.html#digits
  • Code:
    12345.digits      # => [5, 4, 3, 2, 1] -- digits are returned in lowest-position-first order
    0b11010.digits(2) # => [0, 1, 0, 1, 1] -- optional base can be passed
    

ndigits optional argument for rounding methods

Rounding methods of numerics (ceil/floor etc.) now accept an optional argument to specify how much digits to truncate to. If argument is positive, it means decimal digits, and if it is negative, means tens (the same behavior #round had since Ruby 1.9).

  • Discussion: Feature #12245
  • Documentation: Numeric#ceil, Numeric#floor, Numeric#truncate (in fact, Integer and Float classes are affected, because Rational had the same option long ago).
  • Code:
    123.4567.ceil(2)  # => 123.46
    123.4567.ceil(0)  # => 124
    123.4567.ceil(-1) # => 130
    

half: option for #round method

For numbers that are exact half, there are several options provided how to round them: up, down, or to the nearest even number. The default behavior kept unchanged (always up).

  • Discussion: Bug #12958 (discussion of rounding behavior), Feature #12953 (additional rounding options)
  • Documentation: (feature introduced in 2.4, but comprehensive docs were written in 2.5) Integer#round, Float#round, Rational#round
  • Code:
    2.5.round # => 3
    2.5.round(half: :down) # => 2
    2.5.round(half: :even) # => 2
    3.5.round(half: :even) # => 4
    
    25.round(-1, half: :down) # => 20
    
    (13/2r).round(half: :down) # => 6
    

Strings, symbols and regexps

See also chomp: option for String#lines and #each_line, explained in IO section.

Unicode case conversions

All case-conversion methods for String and Symbol support full Unicode since 2.4.

String.new(capacity: size)

When string is created for usage as a mutable buffer for some large textual data, now expected size could be specified, thus optimizing memory allocations.

  • Reason: Ruby’s mutable strings, when used for sequential building of some large text, cause constant reallocations of bigger and bigger memory buffer. By specifying expected capacity beforehand, one can avoid this reallocations.
  • Discussion: Feature #12024
  • Documentation: String::new
  • Code:
    s = String.new(capacity: 10_000_000)
    # => "" -- it is still just an empty string, but internal buffer is already allocated large
    
  • Note: Be careful about subtle difference in encoding, when constructing an empty string:
    # Without source provided, default encoding is ASCII
    String.new(capacity: 10_000_000).encoding # => #<Encoding:ASCII-8BIT>
    
    # When explicitly constructed from empty string, has this string's encoding (defautl to UTF-8 for
    # string literals)
    String.new('', capacity: 10_000_000).encoding # => #<Encoding:UTF-8>
    

#casecmp?

In addition to long-existing case-insensitive comparison method String#casecmp(other) (returning -1, 0, 1 like <=>), new boolean String#casecmp? and Symbol#casecmp? were added.

  • Discussion: Feature #
  • Documentation: String#casecmp?, Symbol#casecmp?
  • Code:
    'test'.casecmp?('Test') # => true
    'test'.casecmp?('Tset') # => false
    'test'.casecmp?(:Test)  # TypeError: no implicit conversion of Symbol into String
    
  • Follow-up: In Ruby 2.5, behavior on incompatible types was changed to return nil, like == does:
    'test' == :Test # => nil
    'test'.casecmp?(:Test) # => nil
    
  • Notes: It was proposed (in the discussion above), but never implemented to allow passing options to casecmp?, making it more precise by specifying locales. Currently, some local characters can produce unexpected results:
    'ı'.casecmp?('I') # => false, though it is Turkish small dotless "I"
    # ...but...
    'ı'.upcase == 'I' # => true
    

String#concat and #prepend accept multiple arguments

  • Discussion: Feature #12333
  • Documentation: String#concat, String#prepend
  • Code:
    "Hello, ".concat('Judy', ', ', 'John', ' and ', 'Paul')
    # => "Hello, Judy, John and Paul"
    'file.mp3'.prepend('dir1/', 'dir2/')
    # => "dir1/dir2/file.mp3"
    

String#unpack1

Just a shortcut for unpack(...).first

  • Discussion: Feature #12752
  • Documentation: String#unpack1
  • Code:
    # Previously, you only could've get an Array
    "\x80".unpack('C')  # => [128]
    
    # Since 2.4
    "\x80".unpack1('C') # => 128
    

#match? method

New boolean methods for checking if some pattern matches some string/symbol.

  • Reason: In the (frequent) situation when only “matches or not” is important, boolean match? is more readable; also, it is more effective because doesn’t set global variables (in case of Regexp#match?) and doesn’t construct MatchData.
  • Discussion: Feature #8110, Feature #12898
  • Documentation: Regexp#match?, String#match?, Symbol#match?
  • Code:
    # before 2.4
    if username =~ /^Admin/
    
    # Ruby 2.4:
    if username.match?(/^Admin/)
    
    # Also supports second parameter: position to search matches from:
    if username.match?(/:admin/, 3)
    

MatchData: better support for named captures

MatchData#named_captures returns the hash of {capture_name => captured string}; MatchData#values_at supports named captures.

  • Discussion: Feature #11999, Feature #9179
  • Documentation: MatchData#named_captures, MatchData#values_at
  • Code:
    m = 'Serhii Zhadan'.match(/^((?<first>.+?) (?<last>.+?))$/)
    # => #<MatchData "Serhii Zhadan" first:"Serhii" last:"Zhadan">
    m.named_captures
    # => {"first"=>"Serhii", "last"=>"Zhadan"}
    m.values_at(:first, :last) # symbols are supported, too
    # => ["Serhii", "Zhadan"]
    m.values_at(0, :first, 2) # as well as a mix of named and numbered
    # => ["Serhii Zhadan", "Serhii", "Zhadan"]
    

Collections

Enumerable#chunk without a block returns an Enumerator

  • Discussion: Feature #2172
  • Documentation: Enumerable#chunk
  • Code:
    ('a'..'k').chunk
    # => #<Enumerator: "a".."k":chunk>
    
    # Example of usage:
    ('a'..'k').chunk.with_index { |e, i| (i % 3).zero? }.to_a
    # => [
    #  [true, ["a"]],
    #  [false, ["b", "c"]],
    #  [true, ["d"]],
    #  [false, ["e", "f"]], ...
    

#sum

Enumerable#sum was implemented as a core alternative of too common reduce(:+)

  • Discussion: Feature #12217
  • Documentation: Enumerable#sum, Array#sum
  • Code:
    (1..5).sum # => 15
    (1..5).sum { |x| x ** 2} # => 55
    
    # Unlike reduce(:+), initial value is implicitly 0, so...
    [].reduce(:+)  # => nil
    [].sum         # => 0
    
    ('a'..'f').reduce(:+) # => "abcdef"
    ('a'..'f').sum        # TypeError: String can't be coerced into Integer
    ('a'..'f').sum('')    # => "abcdef"
    
  • Note: Separate implementation of Array#sum is provided for effectiveness. Important thing to note is it doesn’t rely on Array#each method:
    class MyAry < Array
      def each(&block)
        super { |val| yield val ** 2 }
      end
    end
    
    MyAry.new([1, 2, 3, 4, 5]).sum # => 15, not affected by reimplmented #each
    

#uniq

#uniq method, previously present only in Array, now available for Enumerable and Enumerator::Lazy

  • Discussion: Feature #11090
  • Documentation: Enumerable#iniq, Enumerator::Lazy#uniq
  • Code:
    {a: 1, b: 2, c: 1, d: 2, e: 1}.uniq { |k, v| v }
    #  => [[:a, 1], [:b, 2]]
    
    File.open('very_large_log.log').each_line.lazy.uniq { |ln| ln.scan(/Date: (\S+):/) }.take(10)
    # => first 10 of first-line-of-day
    

Array#max and #min

#max and #min methods of Enumerable reimplemented in Array for speed.

  • Discussion: Feature #12172
  • Documentation: Array#max, Array#min
  • Note: Beware that custom reimplementation of Enumerable#max and #min are now ignored for arrays; and that Array’s implementation doesn’t use #each method.

Array#concat takes multiple arguments

  • Discussion: Feature #12333
  • Documentation: Array#concat
  • Code:
    a = [1, 2]
    a.concat([3, 4], [5, 6]) # => [1, 2, 3, 4, 5, 6]
    a # => [1, 2, 3, 4, 5, 6]
    

Array#pack(buffer:)

When provided with optional buffer: keyword argument, Array#pack uses it as a receiver of data.

  • Reason: a) pre-allocate the memory for big packed data and b) use the same buffer as a target for several chunks of data
  • Discussion: Feature #12754
  • Documentation: Array#pack
  • Code:
    # Old way
    [82, 117, 98, 121].pack('C*')        # => "Ruby"
    [32, 105, 115, 32, 99, 111, 111, 108, 33].pack('C*') # => " is cool!"
    
    # New way
    buffer = String.new(capacity: 30)
    [82, 117, 98, 121].pack('C*', buffer: buffer)
    # => "Ruby"
    buffer # => "Ruby"
    [32, 105, 115, 32, 99, 111, 111, 108, 33].pack('@4C*', buffer: buffer)
    # => "Ruby is cool!"
    buffer # => "Ruby is cool!"
    
    # Note that if the buffer already has content, the unpacked data is appended to it:
    [32, 73, 115, 32, 105, 116, 63].pack('C*', buffer: buffer)
    # => "Ruby is cool! Is it?"
    
    # It can be rewritten with explicit offset 0 directive:
    [32, 73, 115, 32, 105, 116, 63].pack('@0C*', buffer: buffer)
    # => " Is it?"
    

Hash#compact and #compact!

Removes key-value pairs when value is nil.

  • Discussion: Feature #11818
  • Documentation: Hash#compact, Hash#compact!
  • Code:
    data = {name: 'John', age: 34, occupation: nil}
    data.compact # => {:name=>"John", :age=>34}
    data # => {:name=>"John", :age=>34, :occupation=>nil} -- was not affected
    data.compact! # => {:name=>"John", :age=>34}
    data # => {:name=>"John", :age=>34}
    data.compact! # => nil -- if there were nothing to remove
    
  • Note: Notice the last example: when destructive version haven’t changed a hash, it returns nil instead of hash itself. It is consistent with behavior of other destructive methods, and allows writing code like this:
    if data.compact!
      log.info 'Cleaned up the data'
    end
    

Hash#transform_values and #transform_values!

Accepts block to transform each value of the hash.

  • Reason: New method is much easier to write and read, and more effective than
      hash.map { |key, val| [key, do_something(val)] }.to_h
    
  • Discussion: Feature #12512
  • Documentation: Hash#transform_values, Hash#transform_values!
  • Code:
    h = {x: '10', y: '12', z: '54'}
    h.transform_values(&:to_i) # => {:x=>10, :y=>12, :z=>54}
    h.transform_values!(&:to_i) # => {:x=>10, :y=>12, :z=>54}
    h # => {:x=>10, :y=>12, :z=>54}
    h.transform_values!(&:to_i) # => {:x=>10, :y=>12, :z=>54} -- unlike #compact!, always returns self
    
    # Without block, returns Enumerator
    h.transform_values
    # => #<Enumerator: {:x=>"10", :y=>"12", :z=>"54"}:transform_values>
    
    # Can be useful this way:
    h = {manager: 'Jane', reporter: 'John', qa: 'Jane', developer: 'Abraham'}
    h.transform_values.with_index(1) { |v, i| "#{i}: #{v}" }
    # => {:manager=>"1: Jane", :reporter=>"2: John", :qa=>"3: Jane", :developer=>"4: Abraham"}
    
  • Follow-up: Ruby 2.5 also added #transform_keys and #transform_keys!

Filesystem and IO

chomp: option for string splitting

In all contexts where input is split into lines, or received line-by-line, new optional keyword argument chomp: true was added to remove (chomp) line-endings.

  • Reason: Before this change, all line-by-line operations should’ve included chomp as a separate operations when line ending is not needed, which turned out to be most o the cases.
  • Discussion: Feature #12553
  • Documentation (feature introduced in 2.4, but comprehensive docs were written in 2.5-2.6):
  • Code:
    # The effect is the same with String, IO and StringIO, so we are demonstrating just one example:
    require 'stringio'
    io = StringIO.new("foo\nbar\nbaz\n")
    io.gets              # => "foo\n"
    io.gets(chomp: true) # => "bar"
    
  • Notes: What is chomped (and what is lines split on) is controlled by $/ global variable (dubbed $RS or $INPUT_RECORD_SEPARATOR by English module). While quite esoteric by today’s standards, it could be really useful for one-off scripts that work with specific data:
    records = <<~DATA
    First line
    $$$
    Second line
    $$$
    Third line
    DATA
    
    $/ = "\n$$$\n"
    records.each_line(chomp: true).to_a # => ["First line", "Second line", "Third line"]
    
    # The same effect, though, can be achieve with normal `separator` method argument:
    records.each_line("\n$$$\n", chomp: true).to_a # => ["First line", "Second line", "Third line"]
    

#empty? method for filesystem objects

New method empty? was introduced into several classes to check if the file/directory is empty.

  • Discussion: Feature #10121 (Dir), Feature #9969 (File), Feature #12596 (Pathname)
  • Documentation: Dir#empty?, File#empty?, (stdlib) Pathname#empty?
  • Code:
    Dir.empty?('emptydir')    # => true
    Dir.empty?('nonemptydir') # => false
    Dir.empty?('nonexistent') # Errno::ENOENT (No such file or directory @ rb_dir_s_empty_p - nonexistent)
    Dir.empty?('file')        # => false
    
    File.empty?('emptyfile')    # => true
    File.empty?('nonemptyfile') # => false
    File.empty?('nonexistent')  # => false -- unlike Dir.empty?
    File.empty?('dir')          # => false
    
    require 'pathname'
    Pathname('emptydir').empty?    # => true
    Pathname('emptyfile').empty?   # => true
    Pathname('nonexistent').empty? # => false
    Pathname('nonempty').empty?    # => false
    

Thread#report_on_exception and Thread.report_on_exception

Global and thread-local boolean flag to set what should the thread do when ended with exception: die silently (default, old behavior) or print the exception and backtrace to $stderr.

  • Reason: Threads silently dying without any indication could be a lot of confusion, and before this feature top-level exception reporting for each thread should’ve been implemented manually.
  • Discussion: Feature #6647
  • Documentation: Thread.report_on_exception, Thread.report_on_exception=, Thread#report_on_exception, Thread#report_on_exception
  • Code:
    Thread.new { puts 1 / 0 }
    sleep(1)
    # => nothing happens, thread is dead
    Thread.report_on_exception = true
    Thread.new { puts 1 / 0 }
    sleep(1)
    # #<Thread:0x0055b070475fb0> terminated with exception:
    # in `/': divided by 0 (ZeroDivisionError)
    
    # Or instance-level method:
    t = Thread.new { sleep(1); puts 1 / 0 }
    t.report_on_exception = false # silence it again
    sleep(1)
    # => Thread dies in a sad silence.
    
  • Follow-up: Since 2.5, Thread.report_on_exception is true by default.

TracePoint#callee_id

Returns an actual name of the method being called, even if aliased.

  • Discussion: Feature #12747
  • Documentation: TracePoint#callee_id
  • Code:
    tp = TracePoint.new(:call) { |point| p [point.method_id, point.callee_id]}
    
    def real
    end
    
    alias aliased real
    
    tp.enable { aliased } # prints method id and callee id: [:real, :aliased]
    

Stdlib

  • Set: #compare_by_identity and #compare_by_identity methods added, behaving the same way as (existing since 1.9) Hash#compare_by_identity: only elements being the same object (same #object_id) are considered same set element. Discussion: Feature #12210
  • CSV::new: Add a liberal_parsing option, allowing to (try to) parse not-completely-valid CSV. Discussion: Feature #11839
  • Binding#irb start a REPL session like binding.pry.Follow-up: since Ruby 2.5, require 'irb' is not necessary for the feature to work, it is done automatically.
  • Logger::new adds keyword arguments level:, progname:, datetime_format:, formatter:, shift_period_suffix:. The latter allows specifying suffix for filenames on log rotation. Discussions: Feature #12224 (keyword args), Feature #10772 (shift_period_suffix).
  • Net::HTTP.post shortcut method. Discussion: Feature #12375
  • Net::FTP:
    • Support TLS.
    • Support hash style options for Net::FTP.new. While not reflected in the docs, “old” way (as it was before 2.4, with separate args for user, password etc.) still works for backwards compatibility.
    • Net::FTP#status: optional argument pathname (STAT path “is analogous to the “list” command, except that data shall be transferred over the control connection”). Discussion: Feature #12965
  • OptionParser (optparse): OptionParser#parse! and similar methods add into: option to parse, greatly simplifying trivial case of “parse into hash”. Discussion: Feature #11191
    require 'optparse'
    opts = OptionParser.new do |o|
      o.on '-p', '--port=PORT', 'port', Integer
      o.on '-v', '--verbose'
    end
    
    result = {}
    opts.parse!(%w[-p 8080 -v], into: result)
    p result # => {:port=>8080, :verbose=>true}
    
  • Readline: ::quoting_detection_proc and quoting_detection_proc= to specify callable object (Proc or anything responding to #call), customizing the decision “if in this line, character in that position is quoted or not”. It is standard functionality of GNU readline which was not previously exposed by Ruby wrapper. Discussion: Feature #12659

Libraries promoted to default/bundled 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”:

  • default 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;
  • bundled means libraries development also extracted, and they are not packaged with Ruby distribution, just automatically installed with it.

Libraries that became default in 2.4:

Libraries that became bundled in 2.4:

Follow-up: