Speee DEVELOPER BLOG

Speee開発陣による技術情報発信ブログです。 メディア開発・運用、スマートフォンアプリ開発、Webマーケティング、アドテクなどで培った技術ノウハウを発信していきます!

Ruby 2.5.0 リリース直前!何が変わるのかもう一度おさらいしておこう!

この記事は Speee Advent Calender の25日目の記事です。

開発部 R&D グループの村田です。所謂フルタイム CRuby コミッターの一人です。

今日は 12/25 です。数時間後には Ruby 2.5.0 がリリースされている予定です。 皆さんはもう RC1 を試しましたか?え?毎日 trunk をビルドして使っているって?とても素晴らしいですね。

しかし、そうじゃ無い人も大勢いると思います。ですから、ここでは Ruby 2.5.0 で何がどう変わったのか、インパクトが大きいものを選んで最終確認をしておきましょう。

まとめ方は以下のようにしました*1

  • 言語仕様の変更
  • 組み込みクラスの変更
  • パフォーマンス改善

言語仕様の変更

トップレベルで定義された定数の検索

トップレベルで定義された定数はObjectクラスの下に定義されたことになります。

TWO = 2
Object::TWO #=> 2

一方、全てのクラスはObjectクラスを継承しています (親クラスに BasicObject を指定しない限りは)。 そのため、Ruby 2.4 までは、トップレベルで定義された定数を Object クラスのサブクラスの下で定義された定数であるかのようにアクセスできました。しかし、この挙動はプログラマーが意図したものでない可能性が高いため、これまでは次のように警告を出していました。

# Ruby 2.4 まで
class Stuff; end
Stuff::TWO
#=> warning: toplevel constant NUM referenced by Stuff::TWO
#=> 13

Ruby 2.5 では、この機能が削除され、次のように NameError が発生するようになっています。

# Ruby 2.5 から
class Stuff; end
Stuff::TWO
#=> NameError: uninitialized constant Stuff::TWO

この変更によって、const missing をフックして定数の自動定義を実現している場合 (例えば Rails など) に、自動定義される定数と同じ名前の定数をトップレベルに定義することで自動定義が動かなくなる問題が解決しています。

この変更は、定数検索で親クラスを遡る時に Object クラスを特別扱いすることで実現されています。Kernel モジュールや BasicObject クラスはこれまで通りの扱いなので、それらの下に定義された定数はそれらのサブクラスをクラスパスに指定してアクセス可能です。

# これは今までと変わらない
module Kernel
  THREE = 3
end
class BasicObject
  FOUR = 4
end
Stuff::THREE  #=> 3
Stuff::FOUR   #=> 4

参考リンク

do ... end ブロック内に直接 rescue/else/ensure が書けるようになった

この変更は多くの人が嬉しい変更では無いかと私は予想しています。 do ... end 形式のブロックで、 begin ... end で全体を囲まなくてもブロック内で発生した例外を処理する rescue, else, ensure 節が記述できるようになりました。

# Ruby 2.4 まで
lambda do
  begin
    raise 'err'
  rescue
    $! #=> #<RuntimeError: err>
  else
    # no exception case
  ensure
    # finalization
  end
end
# Ruby 2.5 から
lambda do
  raise 'err'
rescue
  $! #=> #<RuntimeError: err>
else
  # no exception case
ensure
  # finalization
end

お、シンタックスハイライトがうまく動いてませんね!!*2 さすが新構文だ。

参考リンク

文字列中の式展開を Refinements でカスタマイズできるようになった

文字列中の式展開は、文字列中に #{...} で埋め込まれた式の評価結果を to_s した結果が展開される機能です。 Ruby 2.4 までは、式展開で呼ばれる to_s メソッドには Refinements が効きませんでした。 この制限は意図的なものではなかったらしく、Ruby 2.5 からは Refinements が効くように変更されました。

参考リンク

制御端末に表示されるバックトレースが見やすくなった

Ruby 2.5 では、制御端末に表示されるバックトレースが今までとは逆順になり、最も最近のメソッド呼び出しが一番下になるような順序で表示されます。 また、視認性が高まるように ANSI エスケープシーケンスを用いて装飾がされるようになりました。 次のスクリプトを実行した結果を見比べて違いを確認してみましょう。

# error.rb
def a; nothing; end
def b; a; end
b

f:id:mrkn:20171225132144p:plain

f:id:mrkn:20171225132214p:plain

この変更は実験的な位置付けとされているので、もしかしたら Ruby 2.6 で廃止になるかもしれませんし、ならないかもしれません。 その点ご注意ください。

参考リンク

組み込みクラスの変更点

Kernel#yield_self が追加された

ブロックの評価結果を返してくれる tap が欲しいなと思っていた皆さん、お待たせいたしました。 とうとうそのためのメソッドの名前が決まり、Ruby 2.5 に追加されました。それが Kernel#yield_self です。

参考リンク

Dir.globbase キーワード引数が追加された

Dir.glob は引数で与えた glob パターンにマッチするディレクトリエントリーの配列を返してくれる便利メソッドです。 このメソッドを使って、ある glob パターンにマッチするエントリーを、特定のディレクトリの内部から探してもらうためには、File.join で基準となるディレクトリとパターンを結合するとか、Dir.chdir で一時的に基準ディレクトリ内部に移動してから Dir.glob するなど、一手間加える必要がありました。

# File.join する方法
Dir.glob(File.join(target_dir, glob_pattern))

# Dir.chdir しちゃうやり方 (これは非スレッドセーフ)
Dir.chdir(target_dir) { Dir.glob(glob_pattern) }

Ruby 2.5 で Dir.glob に追加された base キーワード引数を使うと、このような一手間が不要になります。

Dir.glob(glob_pattern, base: target_dir)

参考リンク

Dir.childrenDir.each_child が追加された

Dir.entries というメソッドをご存知でしょうか。これは、引数で指定したディレクトリの直下にあるエントリーを全て含む配列を返します。 結果として得られる配列には "."".." が含まれるので、ディレクトリ内のエントリーを1つずつ処理したい場合、Dir.entries の要素を1つずつ yield してくれる Dir.foreach を使って次のように書くことが多かったはずです。

Dir.foreach(target_dir) do |entry|
  next if entry == "." || entry == ".."
  process_entry(target_dir)
end

Ruby 2.5 で追加された Dir.childrenDir.entries の配列から "."".." を除いたものを返します。 対応する Dir.each_child は、 Dir.children の要素を1つずつ yield してくれるイテレータです。 これらを用いることで、 Ruby 2.5 では上のコードをより簡潔に以下のように書けるようになりました。

Dir.each_child(target_dir, &method(:process_entry))

参考リンク

Hash#transform_keysHash#transform_keys! が追加された

ActiveSupport が提供する Hashtransform_keystransform_keys! が Ruby 2.5 から組み込みで提供されるようになりました。 これらのメソッドは、Hash のキーをシンボルに統一したい場合などでよく使うんじゃないかと思います。

Ruby 2.4 で Hash#transform_valuesHash#transform_values! が組み込み化されているので、これで両方とも CRuby のコアで提供されるようになりましたね。どちらも提案したのは私です (ドヤァ)。

参考リンク

Hash#slice が追加された

これも ActiveSupport からの輸入です。 Hash から特定のキーに該当するエントリだけを取り出して新しい Hash オブジェクトを作りたいシーンはたまにあります。 JSON や YAML をロードして作ったハッシュから一部を取り出す場合が真っ先に思いつきます。 そのような用途のために ActiveSupport は Hash#slice を提供していました。 Ruby 2.5 からは組み込みで提供されます。

参考リンク

Struct のサブクラスがキーワード引数での初期化に対応した

Struct.newkeyword_init: というオプショナルキーワード引数が追加されました。 これのデフォルト値は false です。これに true を指定すると、生成される Struct のサブクラスの initialize メソッドが、メンバフィールドの値がキーワード引数で与えられることを想定して定義されます。

Foo = Struct.new(:a, :b, keyword_init: true)
Foo.new(a: 1, b: 2)
#=> #<struct Foo a=1, b=2>

参考リンク

Enumerable#any?, #all?, #none?, #one? がパターンの引数渡しに対応した

Enumerableany?all?none?one? は、レシーバ内の各要素に対して与えられたブロックを評価した結果の真偽について、それぞれ次の命題を調べます。

  • any? - 1つ以上が真である
  • all? - 全てが真である
  • none? - 全てが偽である
  • one? - たった1つだけ真であることを判定する

Ruby 2.5 から、これらのメソッドはブロックの代わりに引数でパターンを受け取り、そのパターン p とレシーバ内の要素 e=== メソッドで比較した結果 p === e の真偽について上記の命題の判定を行えるようになりました。

そのため、次のように、ブロックの中で単に正規表現マッチをしているような場合や、クラスオブジェクトと is_a? をしているような場合は、よりシンプルに記述できるようになります。

# Ruby 2.4 まで
strs.all? {|s| /foo/.match?(s) }
nums.none? {|x| x.is_a? Complex }
# Ruby 2.5 から
strs.all?(/foo/)
nums.none?(Complex)

参考リンク

Integer#pow が冪剰余に対応した

Integer#pow が冪剰余 (Modular exponentiation) の算出に対応しました。冪剰余を求めるには、冪剰余の法を第2引数に指定します。

整数 {a}, {b} について、整数 {m} を法とする冪剰余は {y = a^b \pmod{m}} で計算できます。計算結果は {0 \leq y \lt m} ですが、この式をそのまま評価すると {a}{b} が大きい場合に {a^b} の結果を求めるために大きなメモリが必要になってしまいます。冪剰余には {a^b} を求めずに結果を計算できるアルゴリズムが存在し、Integer#pow ではそのアルゴリズムを利用しているため少ないメモリ量で結果を求められます。

参考リンク

Integer がビットマスクのテストに対応

Integer クラスに、3つのインスタンスメソッド allbits?anybits?nobits? が追加されました。これらはビットマスクのテストをします。次の表に意味をまとめました。

意味
n.allbits?(m) (n & m) == m
n.anybits?(m) (n & m) != 0
n.nobits?(m) (n & m) == 0

参考リンク

標準添付ライブラリの変更点

もう require "pp" しなくて良い

Kernel#pp メソッドが、明示的に require "pp" をせずに使えるようになりました。 しかしながら、pp が組み込みで提供されるようになった訳ではありません。 どうなっているかというと、require "pp" して自分自身を本物の pp で上書きしてしまう pp メソッドが組み込みで提供されているのです。 そのため、pp を一切使用しない場合には全くオーバーヘッドを発生させず、pp を使いたいシーンでは勝手に定義をロードしてくれる便利な状態になりました。

参考リンク

coverage ライブラリが条件分岐とメソッド呼び出しのカバレッジの計測に対応した

Ruby 2.5 で、coverage ライブラリが分岐カバレッジとメソッド・カバレッジの計測に対応しました。 ちなみに、これまでは行カバレッジと呼ばれる種類のカバレッジのみ対応していました。

分岐カバレッジに対応したことで、条件分岐の分岐先ごとにカバレッジを計測できるようになりました。 これまでも、以下のように分岐先を異なる行に分けて記述していれば行カバレッジで正しくカバレッジを計測できていました。

if rand(2).odd?
  p :odd
else
  p :even
end

しかし、次のように1行にまとめて記述されていると、行カバレッジでは分岐先ごとのカバレッジは計測できません。

if rand(2).odd? then p :odd else p :even end

分岐カバレッジはこのような場合でも正しく分岐先ごとのカバレッジを計測できます。 分岐カバレッジによってカバレッジ計測が精確になったものは if 式だけでなく、 unless 式、case 式、while 式、until 式、 後置 if 修飾子、後置 unless 修飾子、後置 while 修飾子、後置 until 修飾子、三項演算子 (?:)、 ぼっち演算子こと safe navigation operator (&.) によるメソッド呼び出しが該当します。

メソッド・カバレッジは、メソッド単位でのカバレッジです。 カバレッジ計測としては行カバレッジよりも荒い指標ですが、メソッドが使用されたかどうか、 何回呼び出されたかを確認するときに便利です。

参考リンク

Gemify された標準ライブラリたち

Ruby 2.5 では、次の標準添付ライブラリがデフォルト gem *3になりました。

  • cmath
  • csv
  • date
  • dbm
  • etc
  • fcntl
  • fiddle
  • fileutils
  • gdbm
  • ipaddr
  • scanf
  • sdbm
  • stringio
  • strscan
  • webrick
  • zlib

パフォーマンス改善

trace 命令の削除で全体的に 5〜10% 高速化された

Ruby 2.5 では trace 命令がバイトコードから削除されました。その結果、全体的に実行速度が 5〜10% 高速化されています。 trace 命令は TracePoint のために導入された命令ですが、Ruby 2.5 では動的に命令列を変更する方式が採用されたため trace 命令は不要になりました。 この変更によって、TracePoint を利用しない場合はもちろんですが、TracePoint を利用する場合も若干高速化されたようです。

参考リンク

ブロックパラメータによるブロック渡しが高速化された

ブロックパラメータによるブロック渡しとは以下のようなコードのことです。

def block_pass_through(&b)
  other_method(&b)
end

このように、ブロックを引数で受け取り、それを他のメソッドに受け流しする場合、Ruby 2.5 では Proc オブジェクトの生成を後回しにすることで Ruby 2.4 までと比べて約3倍高速化されました。

参考リンク

ERB のコード生成が高速化された

ERB がテンプレートから実行可能な Ruby プログラムを生成する速度が Ruby 2.4 と比べて約2倍高速化されました。

参考リンク

文字列への長い文字列の埋め込みが高速化された

長い文字列を別の文字列へ埋め込む処理が高速化されました。埋め込み処理の前に必要なメモリを割り当てておくことで、埋め込み処理中に発生するリサイズを抑制し、以下の例では 72% 高速化されています。

参考リンク

組み込みクラスのメソッドが高速化された

組み込みクラスの様々なメソッドが高速化されました。代表例は以下の通りです。

  • Array#concat
  • Enumerable#sort_by
  • String#concat
  • String#index
  • Time#+

まとめ

もうすぐリリースされる Ruby 2.5.0 の変更点の中から、インパクトが大きそうなものを選んで参考リンクを添えて紹介しました。 ここで紹介しなかった変更も沢山あります。NEWS の内容と、リリースされたら公開されるリリースノートも必ず見てください。

みなさんが新しい Ruby とともに楽しく年末年始を過ごせるよう願っています。 ご精読ありがとうございました。

*1:NEWS とだいたい同じ構成です

*2:2017年12月25日現在

*3:RubyGems がgemとしてインストールできるように整備された標準添付ライブラリをデフォルト gem と呼びます。