Edited at

サンプルコードでわかる!Ruby 2.3の主な新機能


はじめに

Ruby 2.3が2015年12月25日にリリースされました。

そこでこの記事ではRuby 2.3の主な新機能を紹介していきます。


対象となるバージョン

以下のとおり、この記事では ruby 2.3.0 を使っています。

$ ruby -v

ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]


参考文献

今回紹介するサンプルコードは下記のサイトにあったコードをベースにしています。

New features in ruby 2.3 - BlockScore Blog

ただし、実行結果を確認するために Minitest を使ったり、コードをいくつか変更したりしています。


サンプルコードはGitHubにあります

この記事で使ったサンプルコードはGitHubに置いてあります。

興味のある方は手元で動かしてみてください。

JunichiIto/ruby-2.3-sandbox

それではここからRuby 2.3の新機能を紹介していきます。


深い階層にあるハッシュの値を一気に取得する Hash#dig

Ruby 2.3で追加された Hash#dig を使うと深い階層にあるハッシュの値を一気に取得できます。

また、Keyが見つからない場合はエラーにならず nil が返ります。

def test_hash_dig

user = {
user: {
address: {
street1: '123 Main street'
}
}
}

# digを使って深い階層の値を一気に取得する
assert_equal '123 Main street', user.dig(:user, :address, :street1)

# Keyが存在する場合は以下のコードと同等
assert_equal '123 Main street', user[:user][:address][:street1]

# 存在しないKeyを指定するとnilが返ってくる(エラーは起きない)
assert_nil user.dig(:user, :adddresss, :street1)

# 普通に[]を使うとエラーになる
assert_raises { user[:user][:adddresss][:street1] }
end


参考:Minitestのコードの読み方

「Minitestで書かれてもわからん!!」という方のために、テストコードの読み方を簡単に紹介しておきます。

# aはbに等しい

assert_equal b, a

# aはnil
assert_nil a

# aは真(true、またはnilでもfalseでもない値)
assert a

# aは偽(nilまたはfalse)
refute a

# aとbは同じインスタンス
assert_same b, a

# a.foo() を実行するとエラーが発生する
assert_raises { a.foo() }

# a.foo() を実行するとFooErrorが発生する
assert_raises(FooError) { a.foo() }


深い階層にある配列の値を一気に取得する Array#dig

ハッシュだけでなく、配列にも dig が追加されています。

考え方はハッシュと同じです。

def test_array_dig

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

# digを使って深い階層の値を一気に取得する
assert_equal 1, results.dig(0, 0, 0)

# Indexが存在する場合は以下のコードと同様
assert_equal 1, results[0][0][0]

# 存在しないIndexを指定するとnilが返ってくる(エラーは起きない)
assert_nil results.dig(1, 1, 1)

# 普通に[]を使うとエラーになる
assert_raises { results[1][1][1] }
end


合致しない要素を返す Enumerable#grep_v

Enumerableモジュールには以前から各要素に対して「引数obj === 要素」を試し、その結果が真だった要素を集めて配列にして返す grep メソッドがありました。

grep (Enumerable) - Rubyリファレンス

Ruby 2.3ではこの逆で「偽だった要素を配列にして返す」grep_v メソッドが追加されました。

# 正規表現 + grep_v を使う場合

def test_grep_v_by_regex
friends = %w[John Alain Jim Delmer]

# Jで始まる文字列を探す
j_friends = friends.grep(/^J/)
assert_equal %w(John Jim), j_friends

# Jで始まる文字列以外を探す
others = friends.grep_v(/^J/)
assert_equal %w(Alain Delmer), others
end

# 型情報 + grep_v を使う場合
def test_grep_v_by_type
items = [1, 1.0, '1', nil]

# Numeric型のオブジェクトを探す
nums = items.grep(Numeric)
assert_equal [1, 1.0], nums

# Numeric型以外のオブジェクトを探す
others = items.grep_v(Numeric)
assert_equal ['1', nil], others
end


参考:grep_vの "v" って何?

おそらく invert(逆にする)の"v"だと思われます。

下記ページにて grep_v の実装が議論されています。

「grepコマンドの-vオプションみたいな機能がほしい」ということから、"grep_v"というメソッド名が提案され、そのまま採択されたようです。


Shota Fukumori


  • sometime I want to do grep -v like operation

  • I'm not sure grep_v is the best name for this feature; feedback are welcomed.

Yukihiro Matsumoto


  • grep_v seems OK. Accepted.

Feature #11049: Enumerable#grep_v (inversed grep) - Ruby trunk - Ruby Issue Tracking System


じゃあ、"grep -v" の "v" は何?ってなるんですが、これは "invert" の "v" っぽいです。


-v, --invert-match


  • Invert the sense of matching, to select non-matching lines.

Linux and Unix grep command help and examples


-i だと 大文字小文字を無視する "ignore case の -i" とかぶってしまうので、-v になったんでしょうね。


複数の値をまとめてfetchする Hash#fetch_values

Hashクラスには以前から fetch メソッドがありました。

fetch メソッドは [] メソッドと機能は同じですが、キーが存在しないときの動作が異なります。

fetch (Hash) - Rubyリファレンス

Ruby2.3では複数のKeyを指定して fetch できる fetch_values が追加されました。

def test_fetch_values

values = {
foo: 1,
bar: 2,
baz: 3,
qux: 4
}

# Keyが存在する場合は values_at も fetch_values も同じ結果を返す
assert_equal [1, 2], values.values_at(:foo, :bar)
assert_equal [1, 2], values.fetch_values(:foo, :bar)

# Keyが存在しない場合、 values_at は nil を返す
assert_equal [1, 2, nil], values.values_at(:foo, :bar, :invalid)

# Keyが存在しない場合、 fetch_values はエラーが発生する
e = assert_raises(KeyError) { values.fetch_values(:foo, :bar, :invalid) }
assert_equal 'key not found: :invalid', e.message

# Hash#fetch と同様、fetch_values はブロックを使ってデフォルト値を返すことができる
assert_equal [1, 2, :invalid], values.fetch_values(:foo, :bar, :invalid) {|k| k }
end


数値の正負を判別する Numeric#positive?, negative?

Ruby 2.3では数値が0より大きいか、0未満かを判断して true/false を返す Numeric#positive?Numeric#negative? が追加されました。

def test_positive_and_negative

numbers = (-5..5)

# positive? メソッドと negative? メソッドを使って、正または負の値を抜き出す
assert_equal [1, 2, 3, 4, 5], numbers.select(&:positive?)
assert_equal [-5, -4, -3, -2, -1], numbers.select(&:negative?)

# negative? は0未満の場合 true を返す、なので負のゼロの場合は false を返す
assert_equal -0.0.negative?, false
end


ハッシュを比較して包含関係を判別する Hash#<=, <, >=, >

Ruby 2.3では、ふたつのハッシュを比較して包含関係を判別する演算子 Hash#<= Hash#< Hash#>= Hash#> が追加されました。

演算子 > / >= / < / <= はそれぞれ集合の関係を表す記号 ⊃ / ⊇ / ⊂ / ⊆ に対応しています。

def test_hash_operators

# A ⊃ B の関係である
assert({ a: 1, b: 2 } > { a: 1 })

# A ⊃ B の関係ではない(値が異なる)
refute({ a: 1, b: 2 } > { a: 10 })

# A ⊃ B の関係ではない(等しい関係である)
refute({ a: 1 } > { a: 1 })

# A ⊇ B の関係である
assert({ a: 1 } >= { a: 1 })

# A ⊃ B の関係ではない(Keyも値も別物)
refute({ b: 1 } > { a: 1 })

# A ⊂ B の関係ではない(Keyも値も別物)
refute({ b: 1 } < { a: 1 })

# A ⊂ B の関係である
assert({ a: 1, b: 2 } < { a: 1, b: 2, c: 3 })
end


ハッシュをProc化できる Hash#to_proc

Ruby 2.3ではハッシュをProc化する Hash#to_proc が追加されました。

これにより、mapの引数にハッシュを渡したりすることができます。

def test_hash_to_proc

hash = { a: 1, b: 2, c: 3 }
keys = %i[a c d]

# ハッシュをProcに変換できるので、mapの引数に &hash を渡せる
assert_equal [1, 3, nil], keys.map(&hash)

# 次のようにProc化したハッシュを個別に呼び出すこともできる
hash_proc = hash.to_proc
assert_equal 1, hash_proc.call(:a)
assert_equal 2, hash_proc.call(:b)
assert_equal nil, hash_proc.call(:d)

# つまり &hash は下のようなコードと同等
assert_equal [1, 3, nil], keys.map { |k| hash_proc.call(k) }

# ハッシュをProc化しない場合は次のようなコードになる
assert_equal [1, 3, nil], keys.map { |k| hash[k] }
end


nil でもメソッド呼び出しがエラーにならない safe navigation operator

Railsの Object#try! のように、オブジェクトが nil でもエラーを恐れずメソッドが呼び出せる safe navigation operator (正式な日本語訳は何?)が追加されました。

safe navigation operator は obj&.foo のように書きます。

objnil であれば、foo の戻り値が nil になります。

require 'active_support/core_ext/object/try'

User = Struct.new(:address)
Address = Struct.new(:street)
Street = Struct.new(:first_lane)

def test_safe_navigation_operator
street = Street.new('123')
address = Address.new(street)
user = User.new(address)
# オブジェクトが存在する場合は普通にメソッドを呼び出せる
assert_equal '123', user&.address&.street&.first_lane

other = User.new
# オブジェクトがnilでもNoMethodErrorが発生せずにnilが返る
assert_nil other&.address&.street&.first_lane

# nil でないオブジェクトに対して存在しないメソッドを呼ぶとエラーが起きる
assert_raises(NoMethodError) { other&.adddress&.street&.first_lane }

# Railsであれば try! を使ったコードと同等
assert_equal '123', user.try!(:address).try!(:street).try!(:first_lane)
assert_nil other.try!(:address).try!(:street).try!(:first_lane)
assert_raises(NoMethodError) { other.try!(:adddress).try!(:street).try!(:first_lane) }
end


参考:Railsの try と try! の違い

try は呼びだしたオブジェクトにそのメソッドが実装されていなくても nil が返ります。

一方、try! はメソッドが実装されていないとエラーが起きます。

Ruby 2.3の safe navigation operator の挙動は try! と同等になります。

assert_nil "hoge".try(:reverseee)

assert_raises(NoMethodError) { "hoge".try!(:reverseee) }
assert_raises(NoMethodError) { "hoge"&.reverseee }

参考文献: Rails - try と try! の使い分け - Qiita

P.S.

@koic さん、編集リクエストありがとうございました!


ヒアドキュメント内のインデントを取り除いてくれる <<~ (squiggly heredoc)

これまでRubyのヒアドキュメントを使うと、ヒアドキュメント内のインデントはそのままスペースやタブとして残っていました。

これだと不便なケースがよくあるため、Railsではこのインデントを取り除いてくれる strip_heredoc というメソッドが用意されています。

Ruby 2.3では <<~ というリテラル(squiggly heredoc)が導入されました。

これを使うと、strip_heredoc と同様に、ヒアドキュメント内のインデントを取り除いてくれます。

require 'active_support/core_ext/string/strip'

def test_squiggly_heredoc
# <<- を使うと、ヒアドキュメント内のインデントがそのままスペースとして残ってしまう
text_with_legacy_heredoc = <<-TEXT
This would contain specially formatted text.
That might span many lines
TEXT
expected = " This would contain specially formatted text.\n That might span many lines\n"
assert_equal expected, text_with_legacy_heredoc

# Railsのstrip_heredocを使うと、インデントのスペースを取り除くことができる
text_with_strip_heredoc = <<-TEXT.strip_heredoc
This would contain specially formatted text.
That might span many lines
TEXT
expected = "This would contain specially formatted text.\nThat might span many lines\n"
assert_equal expected, text_with_strip_heredoc

# <<~ を使うとstrip_heredocと同じようにインデントのスペースを取り除いてくれる(Ruby 2.3)
text_with_squiggly_heredoc = <<~TEXT
This would contain specially formatted text.
That might span many lines
TEXT
assert_equal expected, text_with_squiggly_heredoc
end


文字列リテラルをデフォルトでfreezeさせるマジックコメント(Pragma)

Ruby 3.0では文字列リテラル("abc"'xyz'のようにダブルクオートやシングルクオートで囲った文字列)がデフォルトでfreeze(=不変)になる予定です。

Ruby 2.3ではこれをファイル単位でシミュレーションするためのマジックコメント(Pragma)が導入されています。

【2019.7.14追記】デフォルトで文字列リテラルがfreezeされる変更は見送られました

Ruby 3.0で文字列リテラルがデフォルトでfreezeされる変更は結局見送られたようです。

詳しい内容は以下の記事をご覧ください。


しかし、これに対してRuby3においても frozen string literals をデフォルトとすることはないという判断をしました。

Rubyではimmutable string literalといった非互換な機能の導入はどのようなプロセスを経て決定されますか? - Quora



マジックコメント無し => freezeしない

マジックコメントが付いていないファイルは、文字列リテラルはfreezeしません。

require_relative '../test/test_helper'

class WithoutPragmaTest < Minitest::Test
def test_frozen_string
# マジックコメント(Pragma)がないのでStringは freeze していない
assert_equal "dlrow olleH", "Hello world".reverse!
refute "Hello world".frozen?
end
end


frozen_string_literal: true => freezeする

マジックコメントを付けることで、文字列リテラルがデフォルトでfreezeします。

ただし、文字列リテラルを使わずに文字列を生成した場合は freeze しない点に注意してください。

# frozen_string_literal: true

require_relative '../test/test_helper'

class PragmaEnabledTest < Minitest::Test
def test_frozen_string
# マジックコメント(Pragma)がtrueなのでStringは freeze している
e = assert_raises(RuntimeError) { "Hello world".reverse! }
assert_equal "can't modify frozen String", e.message
assert "Hello world".frozen?

# リテラルを使わずにStringを生成した場合は freeze しない
s = true.to_s
assert_equal 'eurt', s.reverse!
refute s.frozen?
end
end


frozen_string_literal: false => freezeしない

マジックコメントには true/false を指定できます。

false にするとfreezeしません。

# frozen_string_literal: false

require_relative '../test/test_helper'

class PragmaDisabledTest < Minitest::Test
def test_frozen_string
# マジックコメント(Pragma)がfalseなのでStringは freeze していない
assert_equal "dlrow olleH", "Hello world".reverse!
refute "Hello world".frozen?
end
end


実行オプションで挙動を切り替える

ファイル内にマジックコメントがない場合、実行オプションを使って全体の挙動を切り替えることができます。


test.rb

puts "Hello".reverse!


# 実行オプションを使わないとfreezeしない

$ ruby test.rb
olleH

# 実行オプションを付けてデフォルトでfreezeさせる
$ ruby --enable-frozen-string-literal test.rb
test.rb:1:in `reverse!': can't modify frozen String (RuntimeError)
from test.rb:1:in `<main>'

# freezeさせない実行オプションもある
$ ruby --disable-frozen-string-literal test.rb
olleH

実行オプションはそれぞれ --enable=frozen-string-literal --disable=frozen-string-literal のように書くこともできます。


did_you_mean gemの標準添付

NameErrorNoMethodError が発生した際に正しい名前の候補を表示してデバッグしやすくする did_you_mean gemがRuby 2.3から標準添付されます。

特に require 'did_you_mean' のようなコードを書かなくてもデフォルトで有効になっているようです。


test.rb

# わざとメソッド名をtypoする

puts "Hello".revers!

# 実行すると正しいメソッド名の候補を表示してくれる

$ ruby test.rb
test.rb:2:in `<main>': undefined method `revers!' for "Hello":String (NoMethodError)
Did you mean? reverse!
reverse

【2015.11.18 追記:まだあるRuby 2.3の新機能】

@sue738 さんのRuby2.3.0-preview1 リリースノートメモを読みながら、まだ紹介していない新機能があったことに気づきました。

というわけで、さらに追記します。

@sue738 さん、情報ありがとうございます!


バイナリサーチを使って配列のindexを検索する Array#bsearch_index

以前から配列には bsearch というメソッドがありました。

これはバイナリサーチ(二分探索)のアルゴリズムを使って、オブジェクトを検索するメソッドです。

instance method Array#bsearch (Ruby 2.2.0)

Ruby 2.3では見つかった要素そのものではなく、その要素のindexを返す bsearch_index が追加されました。

def test_bsearch_index

# バイナリサーチ(二分探索)を使ってindexを探す
assert_equal 0, [10, 11, 12].bsearch_index {|x| x < 12 }

# 通常のindexを使っても結果は同じ
assert_equal 0, [10, 11, 12].index {|x| x < 12 }

# 配列が予めソートされていない場合、バイナリサーチは間違った結果を返す場合がある
refute_equal 0, [10, 12, 11].bsearch_index {|x| x < 12 }

# 通常のindexであればソートされていなくても問題ない
assert_equal 0, [10, 12, 11].index {|x| x < 12 }
end


参考:パフォーマンスを比較する

上のコードで示したとおり、配列を予めソートしておく必要がありますが、大きな配列であれば index よりも速く検索できる場合があります。

bsearch_indexindex の時間計算量をO記法で表すと次のようになります。


  • バイナリサーチ(bsearch_index) = O(log2 n)

  • 線形探索(index) = O(n)

以下はMinitestのベンチマーク機能を使って、bsearch_indexindex のパフォーマンスを比較したコードです。

require_relative '../test/test_helper'

require 'minitest/benchmark'

class Test < Minitest::Benchmark
def self.bench_range
# 10万、100万、1000万とテストの件数(配列の要素数)を増やす
bench_exp 100_000, 10_000_000
end

def bench_bsearch_index
results = []
# bsearch_index と index の実行結果を保存する
validation = ->(range, times) { results << times }

# bsearch_indexを使って末尾の要素を探す
assert_performance(validation) do |n|
[*0..n].bsearch_index { |item| item == n }
end

# indexを使って末尾の要素を探す
assert_performance(validation) do |n|
[*0..n].index { |item| item == n }
end

# bsearch_index の方が index よりも毎回速いことを検証する
assert results.transpose.all? { |binary, linear| binary < linear }
end
end

# 実行結果(上が bsearch_index 下が index の実行時間)

bench_bsearch_index 0.004181 0.035023 0.361250
bench_bsearch_index 0.006879 0.073163 0.775984

ただし、配列が小さい場合や、検索したい要素が配列の先頭付近にある場合は index の方が速くなる場合もあります。

アルゴリズムの違いを十分理解した上で、適切なメソッドを使用してください。


隣り合う要素が条件を満たせば同じグループとする Enumerable#chunk_while

Ruby 2.3では隣り合う要素が条件を満たせば同じグループとする Enumerable#chunk_while が追加されました。

Ruby 2.2で登場した slice_when でも同じことが実現できますが、「隣り合う要素が条件を満たさなくなったら新しいグループに切り替える」という chunk_while とは逆の条件になっています。

def test_chunk_while

data = [7, 5, 9, 2, 0, 7, 9, 4, 2, 0]
# 隣り合う偶数同士、奇数同士の部分配列ごとに分ける
results = data.chunk_while { |i, j| i.even? == j.even? }.to_a
assert_equal [[7, 5, 9], [2, 0], [7, 9], [4, 2, 0]], results

# slice_whenでも同じことができる。ただし条件が否定形になる
results = data.slice_when { |i, j| i.even? != j.even? }.to_a
assert_equal [[7, 5, 9], [2, 0], [7, 9], [4, 2, 0]], results
end


非推奨となった定数にマークを付ける Module#deprecate_constant

Ruby 2.3では非推奨となった定数にマークを付ける Module#deprecate_constant が追加されました。

マークを付けると、定数の参照時に警告が出力されます。

class ::Foo

BAR = 'Deprecated constant'
# BARは deprecated (非推奨)な定数とする
deprecate_constant :BAR
end

def test_deprecate_constant
# Foo::BAR を参照すると警告が出力される
assert_output(nil, /warning: constant Foo::BAR is deprecated/) { Foo::BAR }
end


エラー発生時に呼び出されたオブジェクトを取得する NameError#receiver

Ruby 2.3では NameError や NoMethodError 発生時に、呼び出されたオブジェクト(レシーバ)を取得する NameError#receiver が追加されました。

def test_name_error_receiver

receiver = "receiver"
e = receiver.to_ss rescue $!
# エラーからレシーバを取得できる
assert_same receiver, e.receiver
assert_instance_of NoMethodError, e

e = foo_bar.to_s rescue $!
# NoMethodErrorの親クラスであるNameErrorでもレシーバを取得できる
assert_same self, e.receiver
assert_instance_of NameError, e
end


freezeされていない/されている文字列を返す+と-

【2017.6.26 追記】 Ruby 2.3からStringクラスに単項演算子の+-が追加されています。

簡単にいうと、+はfreezeされていない文字列を、-はfreezeされた文字列をそれぞれ返却します。

x = +"foo"

x.frozen? #=> false
(-x).frozen? #=> true

y = -"foo"
y.frozen? #=> true
(+y).frozen? #=> false

厳密にいうと、次のような仕様になっています。

String#+@


  • self が freeze されている文字列の場合、元の文字列の複製を返します。 freeze されていない場合は self を返します。

String#-@


  • self が freeze されている文字列の場合、self を返します。 freeze されていない場合は元の文字列の freeze された複製を返します。

引用元:Ruby 2.4.0 リファレンスマニュアル > ライブラリ一覧 > 組み込みライブラリ > Stringクラス


その他の新機能

これ以外にもRuby 2.3ではいろいろなメソッドや機能が追加されています。

が、使い方やサンプルコードの書き方がよくわからなかったので、今回は書きませんでした。(すいません)

もっと詳しく知りたい方は下記のファイルを参照してください。

ruby/NEWS at v2_3_0_preview2 · ruby/ruby


まとめ

というわけで、この記事ではRuby 2.3で導入される主な新機能を紹介してみました。

dig や safe navigation operator なんかは使用頻度が結構高そうです。

did_you_mean gemの標準添付もデバッグ効率を高めてくれそうでありがたいですね!

みなさんもどんどんRubyの新機能を活用していきましょう!


あわせて読みたい

Ruby 2.4の新機能もまとめてみたので、気になる方は以下の記事をご覧ください。

サンプルコードでわかる!Ruby 2.4の新機能と変更点 - Qiita

RSpec学習の定番本、「Everyday Rails - RSpecによるRailsテスト入門」には日本語版独自の特典として 「RSpecユーザのためのMinitestチュートリアル」 という追加コンテンツ(電子書籍)が付いています。

「RSpecならだいたいわかるけど、Minitestはあまりよくわからない」という方には特にオススメですので、まだ読んでいない方はぜひ読んでみてください!

Everyday Rails - RSpecによるRailsテスト入門 + RSpecユーザのためのMinitestチュートリアル