points = []
if (x, y = points.first) # in Ruby <2.4, will raise SyntaxError: multiple assignment in conditional
p [x, y]
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
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
# 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.
Discussions: Feature #9451, Feature #11476
Code:
module Tests
refine Numeric do
def normalize100
clamp(0, 100)
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
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}
using Tests
p [1, 3, 1, 2, 1, 3].tally
# => {1=>3, 3=>2, 2=>1}
using NoRefinements
p Module.used_modules # => [First, Second] -- note NoRefinements absence
Note that order of modules in array returned is not guaranteed.
Warning
module
New single-method module was introduced, meant to be overridden 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}"
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-ups:
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}"
warn 'foo', 'bar'
# Ruby 2.4 prints:
# foo
# bar
# Ruby 2.5 prints:
# .warn called with: "foo\nbar\n"
In Ruby 2.7, new methods were added to Warning
module allowing control over per-category warning suppression;
In Ruby 3.0, category support was improved.
3.3: added new warning category: :performance
.
Reason: Previously, there was 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!"
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.
Follow-up: since 3.0:
clone(freeze: true)
works as expected;
freeze:
argument is passed to initialize_clone
so the object could properly freeze/unfreeze its internal data.
123.clamp(0, 20) # => 20
-123.clamp(0, 20) # => 0
18.clamp(0, 20) # => 18
Follow-up: In Ruby 2.7, clamp
also allows passing range argument, which, especially when combined with endless (2.6) and beginless (2.7) ranges, allows to use more powerful and idiomatic code:
123.clamp(0..20) # => 20
123.clamp(..20) # => 20
-123.clamp(0..) # => 0
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
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 many 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
Unicode case conversions
All case-conversion methods for String
and Symbol
support full Unicode since 2.4.
Discussion: Feature #10085
Documentation: String#downcase
, String#upcase
, String#capitalize
, String#swapcase
, Symbol#downcase
, Symbol#upcase
, Symbol#capitalize
, Symbol#swapcase
,
Code:
'Мамо, ДИВИСЬ, Unicode'.downcase # => "мамо, дивись, unicode"
'Мамо, ДИВИСЬ, Unicode'.downcase(:ascii) # => "Мамо, ДИВИСЬ, unicode" -- ASCII-only processing (old behavior)
'Straße'.downcase # => "straße"
'Straße'.downcase(:fold) # => "strasse" -- Unicode case folding
'TURKIC'.downcase # => "turkic"
'TURKIC'.downcase(:turkic) # => "turkıc" -- Turkic-specific "dotless i" conversion
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
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"
Follow-up: In Ruby 3.1, a second argument was added to unpack1
(and unpack
) allowing to unpack values from the middle of the string.
#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"]
# 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"]], ...
(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 efficiency. Important thing to note is it doesn’t rely on Array#each
method:
class MyAry < Array
def each(&block)
super { |val| yield val ** 2 }
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
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?"
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'
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
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):
String#each_line
, String#lines
,
IO#gets
, IO#readline
, IO#readlines
, IO.foreach
, IO.readlines
,
Standard library: methods of the class affected, but no documentation for the change: StringIO#gets
, StringIO#each_line
, StringIO#readlines
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
$/ = "\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
alias aliased real
tp.enable { aliased } # prints method id and callee id: [:real, :aliased]
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'
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
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:
openssl
webrick
Libraries that became bundled in 2.4:
xmlrpc
Follow-up:
16 more libraries gemified in 2.5;
14 more libraries gemified in 2.6;
16 more libraries gemified in 2.7, and 6 just dropped from the standard library;
34 (!) more libraries gemified in 3.0, and 3 more just dropped from the standard library (including xmlrpc and webrick).