652
654

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-11-15

はじめに

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チュートリアル

652
654
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
652
654

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?