Skip to content

Ruby 2.3 in Detail #13

Open
Open
@JuanitoFatas

Description

@JuanitoFatas

According to the Ruby Version Policy, Ruby will release a new MINOR version every Christmas as a Christmas gift, and so we are expecting 2.3.0 this year. This release brings many new features and improvements and this post helps you catch up on the changes.

General

Did you mean gem is now bundled by default

Have you ever made a typo, this gem shows the possible corrections for errors like NoMethodError and NameError:

"".downcasr
NoMethodError: undefined method `downcasr' for "":String

    Did you mean? downcase
                  downcase!

Safe operator (&.)

This new operator helps to avoid method invocation on nil.

Consider this:

user && user.name

Can now can be reduced to:

user&.name

Without method name will raise SyntaxError:

obj&. {} # syntax error

Only invoked when needed:

user&.address( telphone() ) # telphone() is conditionally evaluated

Useful for attribute assignment too:

user&.sign_in_count += 1

Benchmark

require "benchmark/ips"

def fast(user = nil)
  user&.email
end

def slow(user = nil)
  user && user.email
end

Benchmark.ips do |x|
  x.report("&.")  { fast }
  x.report("check") { slow }
  x.compare!
end
Calculating -------------------------------------
                  &.   155.950k i/100ms
               check   150.034k i/100ms
-------------------------------------------------
                  &.      7.750M (± 9.1%) i/s -     38.520M
               check      7.556M (± 9.2%) i/s -     37.508M

Comparison:
                  &.:  7750478.7 i/s
               check:  7555733.4 i/s - 1.03x slower

Trivia: Matz also calls this the "lonely operator" in his RubyConf 2015 keynote RubyConf 2015 (a person sitting alone and looking at a dot).

Further Reading:

Frozen String Literal Pragma

Add this Magic Comment to the top of a file, and all Strings in the file will then be frozen by default.

# frozen_string_literal: true

This gem can help to add magic comments to all your ruby files.

Alternatively, you can also:

  • Compile Ruby with --enable=frozen-string-literal or --disable=frozen-string-literal
  • Invoke Ruby with ENV variable like so $ RUBYOPT="--enable=frozen-string-literal" ruby

Known Issue:

Related Issue:

Further Reading:

String

<<~ Indented heredoc

sql = <<~SQL
  SELECT * FROM MICHELIN_RAMENS 
  WHERE STAR = 1
SQL

squish

Active Support's String#squish and String#squish! has been ported to Ruby 2.3.

Module

#deprecate_constant

class Deppbot
  GITHUB_URL = "http://github.com/jollygoodcode/deppbot"

  deprecate_constant :GITHUB_URL

  GITHUB_URI = "https://github.com/jollygoodcode/deppbot"
end

Deppbot::GITHUB_URL will throw a warning: warning: constant Deppbot::GITHUB_URL is deprecated.

Numeric

#positive?

42.positive? # => true

Benchmark

require "benchmark/ips"

class Numeric
  def my_positive?
    self > 0
  end
end

Benchmark.ips do |x|
  x.report("#positive?")  { 1.positive? }
  x.report("#my_positive?") { 1.my_positive? }
  x.compare!
end
Calculating -------------------------------------
          #positive?   166.229k i/100ms
       #my_positive?   167.341k i/100ms
-------------------------------------------------
          #positive?     10.098M (± 9.7%) i/s -     50.035M
       #my_positive?     10.094M (± 8.9%) i/s -     50.035M

Comparison:
          #positive?: 10098105.6 i/s
       #my_positive?: 10093845.8 i/s - 1.00x slower

#negative?

-13.negative? # => true

Benchmark

require "benchmark/ips"

class Numeric
  def my_negative?
    self < 0
  end
end

Benchmark.ips do |x|
  x.report("#negative?")  { 1.negative? }
  x.report("#my_negative?") { 1.my_negative? }
  x.compare!
end
Calculating -------------------------------------
          #negative?   154.580k i/100ms
       #my_negative?   155.471k i/100ms
-------------------------------------------------
          #negative?      9.907M (±10.3%) i/s -     49.002M
       #my_negative?     10.116M (±10.4%) i/s -     50.062M

Comparison:
       #my_negative?: 10116334.8 i/s
          #negative?:  9907112.3 i/s - 1.02x slower

Array

#dig

foo = [[[0, 1]]]

foo.dig(0, 0, 0) => 0

Faster than foo[0] && foo[0][0] && foo[0][0][0]

Benchmark

require "benchmark/ips"

results = [[[1, 2, 3]]]

Benchmark.ips do |x|
  x.report("Array#dig") { results.dig(0, 0, 0) }
  x.report("&&") { results[0] && results[0][0] && results[0][0][0] }
  x.compare!
end
Calculating -------------------------------------
           Array#dig   144.577k i/100ms
                  &&   142.233k i/100ms
-------------------------------------------------
           Array#dig      8.263M (± 8.3%) i/s -     41.060M
                  &&      7.652M (± 9.3%) i/s -     37.976M

Comparison:
           Array#dig:  8262509.7 i/s
                  &&:  7651601.9 i/s - 1.08x slower

#bsearch_index

While Array#bsearch returns a match in sorted array:

[10, 11, 12].bsearch { |x| x < 12 } # => 10

Array#bsearch_index returns the index instead:

[10, 11, 12].bsearch_index { |x| x < 12 } # => 0

Please note that #bsearch and #bsearch_index only works for sorted array.

Struct

#dig

klass = Struct.new(:a)
o = klass.new(klass.new({b: [1, 2, 3]}))

o.dig(:a, :a, :b, 0)              #=> 1
o.dig(:b, 0)                      #=> nil

OpenStruct

OpenStruct is now 3X-10X faster in Ruby 2.3 than it was in earlier versions of Ruby.

If you are using Hashie in any of your libraries (especially API wrappers), now is a good time to switch back to OpenStruct.

Hash

#dig

info = {
  matz: {
    address: {
      street: "MINASWAN street"
    }
  }
}

info.dig(:matz, :address, :street) => 0

Faster than info[:matz] && info[:matz][:address] && info[:matz][:address][:street]

Benchmark

require "benchmark/ips"

info = {
  user: {
    address: {
      street1: "123 Main street"
    }
  }
}

Benchmark.ips do |x|
  x.report("#dig") { info.dig(:user, :address, :street1) }
  x.report("&&") { info[:user] && info[:user][:address] && info[:user][:address][:street1]  }
  x.compare!
end
Calculating -------------------------------------
            Hash#dig   150.463k i/100ms
                  &&   141.490k i/100ms
-------------------------------------------------
            Hash#dig      7.219M (± 8.1%) i/s -     35.961M
                  &&      5.344M (± 7.6%) i/s -     26.600M

Comparison:
            Hash#dig:  7219097.1 i/s
                  &&:  5344038.3 i/s - 1.35x slower

#>, #<, #>=, #<=

Check if a hash's size is larger / smaller than the other hash, or if a hash is a subset (or equals to) of the other hash.

Check this spec on ruby/rubyspec to learn more.

Further Reading:

Note that there isn't Hash#<=>.

#fetch_values

Extract many values from a Hash (fetch_values is similar to values_at):

jollygoodcoders = {
  principal_engineer: "Winston",
  jolly_good_coder: "Juanito",
}

> jollygoodcoders.values_at(:principal_engineer, :jolly_good_coder)
=> ["Winston", "Juanito"]

> jollygoodcoders.fetch_values(:principal_engineer, :jolly_good_coder)
=> ["Winston", "Juanito"]

However, fetch_values will throwKeyError` when a given key is not found in the hash:

> jollygoodcoders.values_at(:principal_engineer, :jolly_good_coder, :project_manager)
=> ["Winston", "Juanito", nil]

> jollygoodcoders.fetch_values(:principal_engineer, :jolly_good_coder, :project_manager)
=> KeyError: key not found: :project_manager

Benchmark

require "benchmark/ips"

jollygoodcoders = {
  principal_engineer: "Winston",
  jolly_good_coder: "Juanito",
}

Benchmark.ips do |x|
  x.report("Hash#values_at") { jollygoodcoders.values_at(:principal_engineer, :jolly_good_coder) }
  x.report("Hash#fetch_values") { jollygoodcoders.fetch_values(:principal_engineer, :jolly_good_coder) }
  x.compare!
end
Calculating -------------------------------------
      Hash#values_at   133.600k i/100ms
   Hash#fetch_values   123.666k i/100ms
-------------------------------------------------
      Hash#values_at      5.869M (± 9.4%) i/s -     29.125M
   Hash#fetch_values      5.583M (± 7.7%) i/s -     27.825M

Comparison:
      Hash#values_at:  5868709.9 i/s
   Hash#fetch_values:  5582932.0 i/s - 1.05x slower

#to_proc

hash = { a: 1, b: 2, c: 3 }

[:a, :b, :c].map &hash
=> [1, 2, 3]

Enumerable

#grep_v

This method returns the inverse of Enumerable#grep. Do not confuse this with the $ grep -v command.

#chunk_while

The positive form of Enumerable#slice_when:

> pi = Math::PI.to_s.scan(/\d/).map(&:to_i)
=> [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3]

> pi.slice_when { |i, j| i.even? != j.even? }.to_a
=> [[3, 1], [4], [1, 5, 9], [2, 6], [5, 3, 5], [8], [9, 7, 9, 3]]

> pi.chunk_while { |i, j| i.even? == j.even? }.to_a
=> [[3, 1], [4], [1, 5, 9], [2, 6], [5, 3, 5], [8], [9, 7, 9, 3]]

Additional Resources

Thanks for reading!

@JuanitoFatas ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions