Changes in Ruby 2.4.0-preview1 by examples

by Giang, last updated 19 Jan 2018

Ruby 2.4.0-preview1 has been released recently. I have had a quick look at the changes and done some experiments myself. This post will demonstrate those changes by examples so it will be easier to adapt Ruby 2.4.

Versions used for comparison are Ruby 2.4.0-preview1 and Ruby 2.3.0.

Fixnum and Bignum are unified into Integer

Reference: Feature #12005

Ruby 2.3

  • Integer is just an abstract class
  • Every integer is an instance of either Fixnum or Bignum
# Ruby 2.3
3218489498465156146545613521651651651651657.class
 => Bignum 
69.class
 => Fixnum

Ruby 2.4

  • Integer is a concrete class
  • Fixnum and Bignum are removed and now is an alias name of Integer
  • Every integer is an instance of Integer
# Ruby 2.3
3218489498465156146545613521651651651651657.class
 => Integer
69.class
 => Integer
Fixnum
 => Integer
Bignum
 => Integer

Compatibility Issues

Since Fixnum and Bignum are now alias of Integer, I found this interesting case:

# Ruby 2.3
69.is_a? Bignum
 => false

# Ruby 2.4
69.is_a? Bignum
 => true

Float/Integer#ceil/floor/truncate now take an optional digits

Reference: Feature #12245

# Ruby 2.3
0.12342352.floor 2
0.12342352.ceil 2
0.12342352.truncate 3
 => ArgumentError: wrong number of arguments (given 1, expected 0)

# Ruby 2.4
0.12342352.floor 2
 => 0.12 
0.12342352.ceil 2
 => 0.13 
0.12342352.truncate 3
 => 0.123

Multiple assignment in conditional expression

Reference: Feature #10617

# Ruby 2.3
puts a + b if (a, b = 1, 2)
 => SyntaxError: multiple assignment in conditional

# Ruby 2.4
puts a + b if (a, b = 1, 2)
 => warning: found = in conditional, should be ==
 => 3

String/Symbol supports Unicode case mappings

String/Symbol#upcase/downcase/swapcase/capitalize(!) now work with Unicode characters.

Reference: Feature #10085

Ruby 2.3

"Tiếng Việt".upcase
 => "TIếNG VIệT" 
"Tiếng Việt".swapcase
 => "tIếNG vIệT" 
"TIẾNG VIỆT".downcase
 => "tiẾng viỆt" 
"ê".capitalize
 => "ê"

Ruby 2.4

"Tiếng Việt".upcase
 => "TIẾNG VIỆT" 
"Tiếng Việt".swapcase
 => "tIẾNG vIỆT" 
"TIẾNG VIỆT".downcase
 => "tiếng việt" 
"ê".capitalize
 => "Ê"

String.new(capacity: size)

New option when creating string: capacity: size. This allows the created string growing without the need of recalculating space for that string. There are not many cases where we actually use this.

Reference: Feature #12024

s = String.new capacity: 10000
 => "" 
s = String.new "test", capacity: 10000
 => "test"

Symbol#match now returns MatchData

Reference: Bug #11991

# Ruby 2.3
:abc.match /abc/
 => 0

# Ruby 2.4
:abc.match /abc/
 => #<MatchData "abc">

Array#min/max now faster and independent from Enumerable#min/max

Array#min/max are now defined in Array class instead of being included from Enumerable. Hence when Array#min/max is invoked, call to #each is skipped and makes the call faster (10+ times in ticket benchmark, see reference).

Reference: Feature #12172

A notable consequence is that redefining Enumerable#min/max does not affect Array#min/max anymore. This may lead to incompatible in some of Ruby libs out there. Enumerator is not affected by this change though.

In Ruby 2.3

[1, 2.5, -3].min
 => -3 
[1, 2.5, -3].max
 => 2.5 

module Enumerable
  def min
    'min'
  end

  def max
    'max'
  end
end

[1, 2.5, -3].min
 => "min" 
[1, 2.5, -3].max
 => "max"

In Ruby 2.4

[1, 2.5, -3].min
 => -3 
[1, 2.5, -3].max
 => 2.5

module Enumerable
  def min
    'min'
  end

  def max
    'max'
  end
end

[1, 2.5, -3].min
 => -3 
[1, 2.5, -3].max
 => 2.5

[1, 2.5, -3].to_enum.min
 => "min" 
[1, 2.5, -3].to_enum.max
 => "max"

Another noteable change to Array#min/max is that under certain conditions,[x, y].max and [x, y].min are optimized so that a temporal array is not created. This optiomization is lost when Array#min/max are redefined.

Enumerable#sum & Array#sum

#sum is added to Enumerable hence Enumerator, Range, Hash and Array should have #sum method.

#sum only work with numeric elements, and could receive a block.

Reference: Feature #12217

[1, 2, 3].sum
 => 6 
[1, 2, 3].to_enum.sum
 => 6 
(1..3).sum
 => 6 
[1, 2, 3].sum { |i| i * i }
 => 14 
[1, "2", 3].sum
 => TypeError: String can't be coerced into Integer

Hash#sum always need a block to proceed.

{ 3 => 5, 6 => 7}.sum
 => TypeError: Array can't be coerced into Integer
{ 3 => 5, 6 => 7}.sum { |k,v| k * v }
 => 57

Similar to #min/max, Array#sum is implemented independently from Enumerable.

module Enumerable
  def sum
    'sum'
  end
end

[1, 2, 3].sum
 => 6 
(1..3).sum
 => "sum" 
[1, 2, 3].to_enum.sum
 => "sum"

Note: beware of float

[0.1, 0.2].sum
 => 0.30000000000000004

Dir.empty? & File.empty?

Quickly check if a directory or a file is empty.

References: Feature #10121, Feature #9969

File.empty? "test.csv"
 => false 
Dir.empty? "test"
 => true

Regexp#match?

Reference: Feature #8110

/abc/.match? 'abc'
 => true

Thread#report_on_exception and Thread.report_on_exception

I have not fully understood these functions yet. Follow is the example I guessed from feature discussions & description.

Reference: Feature #6647

# without Thread.report_on_exception
Thread.new { 1 / 0 }
 => #<Thread:0x000000020aadc8@(irb):1 run>

# with Thread.report_on_exception
Thread.report_on_exception = true
 => true 
Thread.new { 1 / 0 }
#<Thread:0x0000000208b298@(irb):3 run> terminated with exception:
(irb):3:in `/': divided by 0 (ZeroDivisionError)
    from (irb):3:in `block in irb_binding'
 => #<Thread:0x0000000208b298@(irb):3 dead>

MatchData#named_captures/values_at

MatchData#named_captures allows matchdata returned as a hash and MatchData#values_at to support #named_captures.

References: Feature #11999, Feature #9179

'12'.match(/(?<a>.)(?<b>.)(?<c>.)?/).named_captures
 => {"a"=>"1", "b"=>"2", "c"=>nil}
'12'.match(/(?<a>.)(?<b>.)(?<c>.)?/).values_at 'a'
 => ["1"]

Final note

I really enjoy the changes in Ruby 2.4 and have a good time writing this post. I hope you have good time reading this too.