プロと読み解くRuby 2.7 NEWS

技術部の笹田(ko1)と遠藤(mame)です。クックパッドで Ruby (MRI: Matz Ruby Implementation、いわゆる ruby コマンド) の開発をしています。お金をもらって Ruby を開発しているのでプロの Ruby コミッタです。

去年の記事「プロと読み解く Ruby 2.6 NEWS ファイル」に続き、今年も本日 12/25 リリース予定の Ruby 2.7 の NEWS ファイルの解説をしてみようと思います。NEWS ファイルとは何か、というのは去年の記事を見て下さい。

実は最近、NEWS ファイルを読みやすくしよう、と例を入れたりしていて、以前のものに比べて読みやすくはなっています(英語だけど)。記事中のコードも、NEWS ファイルから引用しているものがあります。本記事では、変更の解説に加え、執筆者らが開発に携わっているということを活かして、「なぜ変更が入ったのか」という背景を、わかる範囲で紹介していきます。

Ruby 2.7 は、来年 2020 年にリリース予定の Ruby 3 へ移行するために、そこそこ多くの変更が入ったリリースです。また、いつものように、便利な新機能や性能向上が取り込まれています。結構、盛りだくさんになりました。お楽しみ下さい。

他にも Ruby 2.7 を解説している記事があります。我々が見つけられたものだけご紹介。ご参考になさって下さい。

言語の変更

文法など、プログラミング言語 Ruby の意味などについての変更です。

パターンマッチ

データ構造をいい感じにチェック・分解する機能が入りました。

パターンマッチとは

case {a: 0, b: 1, c: 2}
in {a: 0, x: 1}
  :unreachable
in {a: 0, b: var}
  p var #=> 1
end

一見すると、見慣れたcase/whenのように見えますが、case/inなので新構文です。意味もcase/whenと似ていて、{a: 0, b: 1, c: 2}の値にあうパターンを上から探していきます。

in {a: 0, x: 1}は「キーaが存在し、その値は0でなければダメ」かつ「キーxが存在し、その値は1でなければダメ」ということを表現しています。 {a: 0, b: 1, c: 2}は1つめの条件は満たしていますが、xがないので、このパターンにはマッチしません。

マッチしなかったら次のパターンin {a: 0, b: var}を調べます。これは「キーaが存在し、その値は0でなければダメ」かつ「キーbが存在し、その値はなんでもいいので変数varに代入して」ということを表現しています。 これは両方の条件を満たすのでマッチし、varに1を代入した上で、この中の節(ここではp var)を実行する、ということになります。

もしどのパターンにもマッチしなかったら、NoMatchingPatternError例外が投げられます。ここはcase/whenと違うので注意してください。

具体的なユースケースとしては、JSONデータが期待した構造になっているかチェックし、そこから必要なデータを一気に取り出す、というようなときに使えるでしょう。もう#digに頼らなくてもいいんだ。

json = <<END
{
  "name": "Alice",
  "age": 30,
  "children": [{ "name": "Bob", "age": 2 }]
}
END

JSON.parse(json, symbolize_names: true) in 
  {name: "Alice", children: [{name: child_name, age: age}]}

p child_name #=> "Bob"
p age        #=> 2

パターンマッチの詳細を説明しだすと長いので、詳しくはRubyのパターンマッチを設計・実装した辻本和樹さんによる資料を見てください(ちょっと古いところもあります)。

パターンマッチ導入の何が困難だったか

さて、ここからは変更の背景です。

パターンマッチは主に静的型付き関数型プログラミング言語で使われている機能です。Rubyで模倣したり提案したり試作したりということは古くから行われていて、待望されていたと言えます。

しかし、言語組み込みにふさわしい構文がなかなか提案されませんでした。というのも、Rubyの構文は柔軟すぎて拡張の余地が少なく、かといって新たなキーワードを導入するのは互換性の観点で難しく、その上、パターンマッチのパターンは基本的にそのデータを作る構文に似たものにする(配列だったら[x, y, z]、ハッシュだったら{a: x, b: y})という慣習もあり、なかなか期待にあう構文が発見できなかったのでした。

この状況を打破したのが辻本さんでした。辻本さんはinというキーワードを再利用することを提案しました。Rubyには繰り返しの構文for ... inがあり(現代ではほとんど使われない構文です)、この構文のためにinはすでにキーワードだったので、新たに導入する必要はありません。パターンマッチを表現するキーワードとしてベストかどうかは議論の余地があるものの、case/inという構文はそれなりに直感的であり、パターンマッチ導入の現実味が高まりました。

辻本さんが2018年に文法と意味のたたき台を作ったことで議論が本格化し、2019年になって実装され、RubyKaigi 2019のタイミングでコミットされ、半年以上の実験と議論を重ねて、無事2.7に入ります。

ただし、まだあくまで実験的導入という位置づけであり、利用すると次のように警告が出ます。

$ ./miniruby -e 'case 1; in 1; end'
-e:1: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

今後、より広く使ってもらって細かい改善を経て安定していくものと思います。プロダクションのコードに入れるのはやりすぎかもしれませんが、「実験段階なら使うの避けるか」とか思わず、ぜひ試してフィードバックいただければと思います。

(文責:mame)

Ruby 3のキーワード引数分離に向けた警告

  • Automatic conversion of keyword arguments and positional arguments is deprecated, and conversion will be removed in Ruby 3. [Feature #14183]

Ruby 3では、「キーワード引数分離」という非互換が予定されています。Ruby 2.7では、Ruby 3から動かなくなるコードを警告するようになりました。

Ruby 2のキーワード引数の功罪

Ruby 2のキーワード引数は、ただのハッシュの引数として渡されます。これはRuby 1時代の慣習を引き継いだもので、当時としては自然な拡張だったと思います。しかし、この設計は数多くの非直感的挙動を生む罠でした。

なにが問題かと言うと、呼び出されたメソッド側からはキーワードだったのかハッシュだったのか区別できないことです。具体例で示します。

def foo(x, **kwargs)
  p [x, kwargs]
end

def bar(x=1, **kwargs)
  p [x, kwargs]
end

という、そっくりなメソッドを2つ定義します。これらにハッシュを渡して呼び出します。

foo({}) => [{}, {}]
bar({}) => [1, {}]

挙動が違ってビックリしませんか。メソッド側からは、最後の引数がハッシュオブジェクトだったのかキーワードだったかわからないので、「必須引数>キーワード引数>オプション引数」という微妙な優先度で解釈をします。2.0リリース当初は「キーワード引数>必須引数>オプション引数」でしたが、バグ報告が来たので微妙に変更されました。

barにオプション引数として{}を渡すにはどうすればいいでしょうか。bar({}, **{})というのを思いつくかもしれません。しかし、Ruby 2.6ではこれは期待に反する結果となります。

bar({}, **{}) => [1, {}]

**{}は「何も指定しないのと同じ」とみなされ、1つめの{}がキーワードとして解釈されてしまうためです。barに引数{}を渡すには、bar({}, {})と呼ぶのが正解でした。こんなのわかるわけ無いですね。

なお、当初の2.0では「**{}は一貫して{}を渡す」という意味でしたが、「**{}は無と同じであるべき」というバグ報告が来たので後から変更されました。何かを直すと新たな非直感が生まれる、というのをRuby 2のキーワード引数は繰り返し続けています。

Ruby 3でのキーワード引数

Ruby 2の問題は、キーワード引数を単なるハッシュとして渡すという基本設計に起因しています。Ruby 3ではここを根本的に直します。つまり、キーワード引数とただの引数を分離します。

Ruby 3では、foo({})は一貫して普通の引数を渡します。foo(**{})は一貫してキーワード引数を渡します。完璧にわかりやすいですね。

# in Ruby 3
foo({}) #=> [{}, {}]
bar({}) #=> [{}, {}]

foo(**{}) #=> wrong number of arguments (given 0, expected 1)
bar(**{}) #=> [1, {}]

しかしこのために、キーワード引数を渡すつもりでfoo(opt)などと書いていたコードは動かなくなってしまいます。ここはfoo(**opt)と書き直す必要があります。

そこでRuby 2.7は、原則としてRuby 2.6と同じように動きますが、このように動かなくなる呼び出しをやったら警告を出すようになっています。

def foo(**kw)
end

foo({}) #=> test.rb:4: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
        #   test.rb:1: warning: The called method `foo' is defined here

この警告が出たら、**を足すなどの対応をしないとRuby 3では動かない、ということになります。

キーワード引数分離についてより詳しくは

これだけなら簡単なのですが、キーワード引数分離の移行のための書き換えは、しばしばもっと難しいときがあります(特に委譲がからむケース)。Rubyの公式サイトに移行ガイドを掲載しているので、見てみてください。

キーワード引数分離の裏話

これは懺悔ですが、Ruby 2でキーワード引数を実装したのは私(遠藤)です。言い訳すると、多少違和感のあるコーナーケースがあることもわかっていたのですが、Ruby 1からあったハッシュのブレースを省略する関数呼び出し(foo(:a => 1, :b => 2))の自然な拡張なので、互換性を考慮すると悪くない妥協であると思ってました。

しかし、2.0リリース後に多数の非直感的挙動が報告され、そのたびにどんどん複雑怪奇度を増していってしまいました。言語設計をバグ報告ベース、コミュニティベースでアドホックに進めると失敗する例。

このことはずっと後悔していて、Ruby 3で直せるなら直したいと思っていました。クックパッドでフルタイムコミッタになったころに、このことをmatzに話したところ、matzも同じように思っていたようで、RubyWorld Conference 2017やRubyConf 2017などで変更することが宣言されました。

2017年の終わり頃からmatz、akr、ko1、自分の4人で、問題点の整理やRuby 3の設計、移行パスの立案に取り組んでいました。遠藤は、設計案ができたら試作して、題材としてクックパッドのRailsアプリを実行してみて、影響を測る、というのを繰り返しました。その結果はチケット [Feature #14183] にも残っています。

当初は完全な分離を目指していましたが、あまりにも非互換が大きい(def foo(opt = {}); endというメソッドにfoo(k: 1)を渡すコードはあまりにも多い)ということをチケット上でJeremy Evansという人が主張し、このケースだけは分離を諦めることでチケット上で合意しました。これが2019年4月ごろ。

Jeremy Evansはこの議論や実験をきっかけにコミッタになりました。彼は単に主張するだけではなく、非常に精力的に実装や実験もやってくれました。後半の実装の多くは彼の手によります。感謝。

(mame)

Numbered parameters

  • Numbered parameters as default block parameters are introduced. [Feature #4475]

  • You can still define a local variable named _1 and so on, and that is honored when present, but renders a warning.

Numbered parameters という、ブロックパラメータの宣言を省略できる機能が導入されました。

ary = [1, 2, 10] という配列 ary の中身をそれぞれ、16進表記する文字列に変換する処理は、ary.map{|e| e.to_s(16)} と書くことが出来ます。このブロックパラメータ e という変数名は、elementという意味で、名前を考えるのが面倒なときに、私が適当に選ぶ変数です。ただ、整数なんだから、integer の |i| も捨てがたいですね。いやいや、number の |n| もいいかもしれません。どれにしましょうか。うーん。名前付けは面倒くさい。

そう、名前をつけるのは面倒くさいのです。簡単なプログラム(配列の中身を16進表記に変換するのは、Ruby ではとても簡単なプログラムでしょう)で、イチイチ考えるのはいやなのです。

そこで、Numbered parameter という新機能が導入されました(Feature #4475: default variable name for parameter)。ブロックの引数を、_1_2として、名前を付けずに参照できるという機能です。先ほどの16進表記への変換は、ary.map{_1.to_s(16)} と書くことができます。

ary.map{|e| e.to_s(16)}
ary.map{_1.to_s(16)}

並べてみると、3文字ほど省略できているのがわかります。まぁ、文字数よりも、書く時に名前を考える手間が減るのが、嬉しいところかと思います。

Numbered parametersの細かい話

書いてみるとわかると思うんですが、沢山 _1_2 と並んでいると、わけがわからなくなります。本当にちょっとしたプログラムにしか用いないように注意するといいでしょう。もう、誰か RuboCop のプラグイン、書いてますかね? また、他の人に適切に意味がわかるようにしなければならない時も、使わない方がいいでしょう。わかりやすいプログラムは、適切な変数名から。

ブロックが入れ子になっていてもわかりづらいので、ブロックの入れ子で Numbered parameter を利用できるのは、ある一つのブロックのみとなっています。例えば、最内ブロックの中で使うようにするといいでしょう。もし使っちゃうと、エラーになります。

3.times do
  _1.times do
    p _1
  end
end

#=>
# t.rb:3: numbered parameter is already used in
# t.rb:2: outer block here
#     p _1
# ^~~~~~~~

エラーメッセージ、わかりやすいですね。

なお、ブロックで第一引数を利用するとき、iter{|e|}と、iter{|e,|} という、よく似た二つの表記を利用することができます。ブロック引数の宣言での |e||e,| の違い、つまりコンマがついているかいないかです。まぁ、あんまり気にしなくていいと思うし、面倒なので細かい解説はしませんが、ブロック中に_1が1個しか書いていなければ、|e| の意味になります。

_1 は、ローカル変数やメソッド名に使える普通の変数です。が、今後は利用しないほうが良いでしょう。すでにそう書いたプログラムがある場合は、変更をお勧めします。

もし、ブロックの外側で _1 というローカル変数があれば、それは外側のスコープの変数として利用され、暗黙のブロック引数にはなりません。ただし、_1 = ...という式に対して警告が出るようになっています。

_1 = 0
#=> warning: `_1' is reserved for numbered parameter; consider another name
[1].each { p _1 } # prints 0 instead of 1

Numbered parameters についての議論

この機能自体は、結構昔から議論されていました。ただ、なかなか決まりませんでした。主に、表記と機能の問題です。

例えば、Groovy という言語には it という、今回の _1 相当の機能がありました。ただ、Ruby では RSpec などですでに利用されている識別子です。また、第一引数のみで良いのか、という議論がありました。個人的には、Scheme(SRFI 26) の <>が良かったんですが、!=に見える、みたいな意見もありました。

ただ、なかなか決まらなかったところ、@1, @2, ... というのでいっか、と、あるときの Ruby 開発者会議(毎月1回、まつもとさんと仕様を検討する会議です)でストンと決まりました。なお、このときは @1 だけ利用するときは |e,| と同じ意味でした。

その後、表記について、いろいろな議論がありました。例えば Misc #15723: Reconsider numbered parameters - Ruby master - Ruby Issue Tracking System というチケットでは、129 個のコメントが集まってますね。

また、遠藤さんが、機械的にブロック引数を @1, ... に変更して表記を確認してみる、といった実験をしてくださいました(https://twitter.com/mametter/status/1159346003536838656)。これを見て、まつもとゆきひろさんは、@1 ってちょっとインスタンス変数っぽすぎるな、と言ってました。

そんなこんなで、代わりに _1, _2, ... という表記となりました。また、圧倒的に |e,| ではなく、|e| として利用することが多いため、_1 だけの利用では、|e| と書いているのと同じ意味になりました。いやぁ、決まるまで長かった。

この機能って、結局ブロックをいかに簡単に書くか、という話なんですよね。上記16進表記への変換では、例えば16進表記文字列へ変換する Integer#to_s16があれば、ary.map(&:to_s16) と書くことができます。みんな大好きなアレですね。しかし、ないので ary.map{_1.to_s(16)} と書けると、そういうブロックを簡単に書くニーズに応えられるんじゃないか、という理由で導入されました。他の案としては、引数16を渡したProcを生成する仕組みを提供するのはどうか、といった提案もありました。例えば ary.map(&:to_s(16)) ですね。こんなふうな色々な表記のリクエストがきており、それらをだいたい解決するかな、ということで、今回の numbered parameter (_1, ...)が導入されました。

この辺、関数型言語っぽい機能をRubyで使うための仕組みを、すっきりデザインしてくれる人がいれば、また違った機能が入るかも知れません。

(ko1)

proc/lambdaをブロックなしで呼ぶのは非推奨/禁止になりました

  • Proc.new and Kernel#proc with no block in a method called with a block is warned now.
  • Kernel#lambda with no block in a method called with a block raises an exception.

proc{...} とすれば、Procオブジェクトを生成できます。さて、ブロックを渡さないとどうなるか知ってますか?

def foo
  proc.call #=> 1
end

foo{p 1}

実は、ブロックを指定しないと、procを呼び出したメソッドにわたってきたブロックを、Procオブジェクトとして返します。

で、この機能は、そもそもブロック渡し引数がないときのデザインだったので、もうこの機能は辞めましょう、というのが今回の変更になります。使うと、warning: Capturing the given block using Kernel#proc is deprecated; use '&block' instead という、警告が出ます。

lambdaは以前から警告が出ていたのですが、例外(ArgumentError)が出るようになりました。

ブロック無し proc/lambda を使わない書き方

今後は、ブロック渡し引数を用いて、

def foo &b
  b.call #=> 1
end

foo{p 1}

こんなふうに書き直してください。

なお、あるメソッドが、引数で Procを渡すか、もしくはブロックを渡すかを選択できるメソッドを定義するとき、ブロック無し proc が利用されていました。

def foo(pr = proc)
  pr.call
end

foo(proc{p 1}) #=> 1
foo{p 2}       #=> 2

これをブロック渡し引数だけで再現することはできません。

def foo pr = nil, &b
  pr = pr || b
  pr.call
end

こんな感じで、1行余分に条件文を付けることで対応可能です。まぁ、読みづらいので、どちらも受けるという API は辞めていくのがいいのではないでしょうか。

ブロック無し proc/lambda 禁止の背景

この修正が行われたきっかけは、実はまつもとゆきひろさんの提案(入ってない)で、ブロックを受け付けないメソッドを def foo(&nil) と定義できるようにするとどうだろうか、というものでした。この提案は、ついうっかり間違えてブロックを使わないのにブロックを渡してしまう、というミスを回避することを目的としていました(間違えて、p{...} とか、書いたことありませんか?)。ただ、これを入れると、「ブロックを用いないすべてのメソッド定義」、つまり大部分のメソッド定義に &nil を付けまくる、勤勉な風習が増えそうだったので、全力で反対しました。

その代わりに、インタプリタがメソッドでのブロックの利用をチェックし、ブロックを利用しないメソッドにブロックを渡していたら、警告もしくはエラーにすれば良さそうです。ちょっと試すと、2つくらいバグを見つけることができました([Feature #15554])。ただ、この提案はいくつか問題があって、入っていません。というのも、実際のプログラムで、いくつか意図的に「メソッドでは使わないのにブロックを渡す」というプログラムが発見されたためです。Ruby 3 に期待。

さて、あるメソッドがブロックの利用の可否を判断するために、いくつか障害がありました。その一つが今回のブロック無し proc です。proc はブロックを用いますが、インタプリタからは、ただのメソッドに見えます。proc という名前のメソッド呼び出しが、本当に Kernel#proc なのか、コンパイル時に判断する方法は Ruby にはないので、きちんとわからないのです。

さて、そういう背景もあり、とりあえず前々からいらんのでは、と言われていたブロックなし proc(と lambda)は obsolete となったのでした。

(ko1)

beginless range

  • A beginless range is experimentally introduced. It might not be as useful as an endless range, but would be good for DSL purpose. [Feature #14799]

Ruby 2.6でendless rangeが入ったので、次はbeginless rangeが入りました。

ary = [1, 2, 3, 4, 5]
p ary[..2] #=> [1, 2, 3]

endless rangeと違い、beginless rangeは#eachができないので、値の範囲を表現するための用途に限られるでしょう。

Companies.where(sales: ..100)

1.clamp(0..)

case age
when (...18)
  "未成年"
when (18...)
  "成人"
end

(mame)

特殊変数$;$,の廃止

  • Setting $; to non-nil value is warned now. Use of it in String#split is warned too. [Feature #14240]
  • Setting $, to non-nil value is warned now. Use of it in Array#join is warned too. [Feature #14240]

Perlから引き継いだ特殊変数を廃止する動きの一環で、$;$,の使用が警告されるようになりました。どういう変数だったか調べるのも面倒ですが、String#splitArray#joinに関係があるものだったのだと思います。

このように、そもそも知られていないし使われてもいないことに加え、String#splitなどの挙動をグローバルに変更するため、ライブラリが想定外の動きをする可能性があり危険である、というのも廃止の理由です。

(mame)

引用ヒアドキュメントの識別子は改行禁止

  • Quoted here-document identifier must end within the same line.

ヒアドキュメントで、識別子をクオートで囲むことができます。ちなみに、<<'EOS' のように書くと、文字列の埋め込みを禁止することができます。さて、この EOS にあたる部分は、実は改行を含むことが許されていました。ちょっと何を言っているかわからないと思いますが、

<<"EOS
" # This had been warned since 2.4; Now it raises a SyntaxError
EOS

こういうコードが書けたわけです。この場合は、EOS\nが区切り文字になりました。Ruby 2.4 から、このようなプログラムは警告が出ていましたが、Ruby 2.7 ではエラーにする、という変更になります。利用例とかあったのかなぁ。

(ko1)

flip-flop が戻ってきた

Ruby 2.6 で flip-flop は obsolete となりましたが、「まだ使ってるよ!」という声が根強かったので(多分)、戻ってきました(obsolete ではなくなりました)。ファンの方、おめでとうございます。声はあげてみるものですね。

(ko1)

.bar のようなメソッドチェインを一部コメントアウト可能に

  • Comment lines can be placed between fluent dot now.
    foo
      # .bar
      .baz # => foo.baz

こんなふうに、メソッドチェイン foo.bar.baz を、. の前に改行を挟んで記述しているとき、.bar の部分だけコメントアウトしたい、ということが、試行錯誤しているときとかありそうです。以前は、そこだけコメントアウトすると文法エラーとなっていましたが、Ruby 2.7 では foo.baz の意味になるようになりました。

ちなみに、コメント行ではなく、空行だと文法エラーです。

foo

  .bar
  #=> syntax error, unexpected '.', expecting end-of-input

(ko1)

self. を付けてもプライベートメソッドが呼べるようになった

プライベートメソッドはレシーバを付けて呼び出すことができませんでした。すなわち、プライベートメソッド foo を、 recv.foo のように呼ぶことはできませんでした。self.foo も同様に駄目でした。ただ、いくつかの理由から、self. くらいつけたい、という用途があって、Ruby 2.7 ではそれが許されるようになりました。

self.p 1
#=>
# Ruby 2.6: t.rb:1:in `<main>': private method `p' called for main:Object (NoMethodError)
# Ruby 2.7: 1

なお、正確に self. とレシーバを書かなければならず、s = self のような変数を使って s.foo としてもプライベートメソッドは呼べません。

この機能により、今まで self.private_method と呼ぶと、method_missing が呼ばれていたのが、素直に呼べるようになったので method_missing が呼ばれなくなるという、若干の非互換が入りました。そこに依存したプログラムがあるとは思いたくない...。

(ko1)

多重代入における後置 rescue の優先度の変更

  • Modifier rescue now operates the same for multiple assignment as single assignment. [Bug #8279]
    a, b = raise rescue [1, 2]
    # Previously parsed as: (a, b = raise) rescue [1, 2]
    # Now parsed as:         a, b = (raise rescue [1, 2])

コメントにある通りなんですが、後置 rescue が、多重代入式で使われたとき、どの部分にかかるか変わりました。変更後の括弧の位置は、想定と同じでした?

私、後置rescueは難しくて使わないんですよねえ。どの例外をキャッチするんだっけ、とかすぐわからなくなっちゃって。

(ko1)

シングルトンクラスの中で yield は廃止予定

  • yield in singleton class syntax is warned and will be deprecated later [Feature #15575].

何を言っているのかわからないだろうし、わかっても、なんでここで yield すんねん、という感じですが、次のようなコードは Ruby 2.6 以前で動きます。

   def foo
     class << Object.new
       yield
     end
   end
   foo { p :ok } #=> warning: `yield' in class syntax will not be supported from Ruby 3.0.

が、わけわかんないのでやめましょう、と警告が出ます。やらないよね? こんなの。Ruby 3 では文法エラーになる予定です。

変更の背景は、実はブロック無し proc 禁止と同じです。

(ko1)

引数を転送する記法 (...)

受け取った引数をそのまま他のメソッドに転送するための記法が導入されました。

def foo(...)
  bar(...)
end

受け取る方も渡す方も両方とも(...)でないと構文エラーになります。

従来は次のように書いていたと思います。これは煩わしいことに加え、最適化がしにくかったり、Ruby 3ではさらに**optも受け渡さないといけなくなったりするということで、導入されました。

# Ruby 2
def foo(*args, &blk)
  bar(*args, &blk)
end

# Ruby 3
def foo(*args, **opt, &blk)
  bar(*args, **opt, &blk)
end

# (...)を使った場合(Ruby 2.7とRuby 3以降の両方で動く)
def foo(...)
  bar(...)
end

なお、「Rubyのメソッド呼び出しはカッコが省略できる」と覚えている人も多いと思いますが、この記法はカッコが必須です。なぜかというと、bar ...はendless rangeと解釈されてしまうためです。

2020-02-21編集:「引数を委譲する記法」と呼んでいましたが、「引数を転送する記法」に変えました。

(mame)

$SAFE の廃止

  • Access and setting of $SAFE is now always warned. $SAFE will become a normal global variable in Ruby 3.0. [Feature #16131]

Ruby がもつ古のセキュリティ機構である $SAFE が廃止されました。そもそも、$SAFE って知ってますか?

$SAFE は、基本的には信頼できないデータ(文字列など)にフラグを付けておいて(taint フラグ)、systemopen といった、危ない操作っぽいものをしようとしたら、インタプリタが「危ないからやめて!」と止めてくれる(SecurityError が出ます)という機能です。

ただ、現代のフレームワークでは、$SAFE について考慮せず、適切な taint フラグの付与が行われないなど、$SAFE が実質的に利用可能ではなくなっていました。このような不完全な状況で、$SAFE を間違って信頼してしまうとセキュリティ上問題なので、いっそ消してしまおう、というのが今回の提案です。

$SAFE に何か代入しても、下記のような警告が出ます。

$SAFE = 1
#=> t.rb:1: warning: $SAFE will become a normal global variable in Ruby 3.0

Ruby 3.0 では、$SAFE はただのグローバル変数に戻るとのことです。てっきり、永久欠番みたいな扱いにするのかと思ってました。

  • Object#{taint,untaint,trust,untrust} and related functions in the C-API no longer have an effect (all objects are always considered untainted), and are now warned in verbose mode. This warning will be disabled even in non-verbose mode in Ruby 3.0, and the methods and C functions will be removed in Ruby 3.2. [Feature #16131]

この変更に伴い、Object#taint などのメソッドは、何も効果がなくなります。つまり、taint フラグのついたオブジェクトは存在しないと言うことです。Ruby 3.2 でこれらのメソッドは削除されるようなので、早めに対処しましょう。

(ko1)

Object#methodなどでRefinementsを考慮するようになった

  • Refinements take place at Object#method and Module#instance_method. [Feature #15373]

今まで、Object#methodなどで取り出すメソッドは、Refinementsを気にしていませんでしたが、ちゃんと気にするようになりました。

# [[Feature #15373]](https://bugs.ruby-lang.org/issues/15373) から一部変更して引用

# default call to #pp
module P2PP
  refine Kernel do
    def p obj
      pp obj
    end
  end
end

using P2PP

method(:p).call ['1' * 40, '2' * 40]
#=>
# Ruby 2.6: オリジナルの p が呼ばれる
#   ["1111111111111111111111111111111111111111", "2222222222222222222222222222222222222222"]
# Ruby 2.7: Refinements (using) が効いて pp になる
#   ["1111111111111111111111111111111111111111",
#    "2222222222222222222222222222222222222222"]

(ko1)

組込クラスのアップデート

Array#intersectionの導入

配列の共通の要素だけを取り出すArray#intersectionが導入されました。もともとあったArray#&と大体同じです(3つ以上の配列のintersectionも取れるところがちょっと違う)。

ary1 = [1, 2, 3, 4, 5]
ary2 = [1, 3, 5, 7, 9]

p ary1.intersection(ary2) #=> [1, 3, 5]

Ruby 2.6でArray#|に対応するものとしてArray#unionが導入されましたが、intersectionは要望がなかったため見送られていました。が、今回要望が来たので入りました。Rubyの開発はたまに偏執的なほど要望ベースで動きます。

(mame)

Array#minmax, Range#minmax の性能向上

  • Added Array#minmax, with a faster implementation than Enumerable#minmax. [Bug #15929]

ary.minmax を実行すると、Enumerable#minmax が実行されましたが、Array#minmaxを別途用意することで、より高速に実行することができるようになりました。#each を使わないからですね。

Enumerable + each で良い感じに全部定義できる、というのはわかりやすいけど、現実的にはこういう変更が入ります。言語処理系開発者としては、本当はこうしなくても速くできるといいんですけどね。

  • Added Range#minmax, with a faster implementation than Enumerable#minmax. It returns a maximum that now corresponds to Range#max. [Bug #15807]

同じように、Range#minmaxも別途用意されました。なお、最大値を算出するアルゴリズムが、Enumerable#maxではなく、Range#maxを用いるため、もしかしたら非互換が出るかも知れません。

(ko1)

Comparable#clampがRange引数に対応

-1.clamp(0..2) #=> 0
 1.clamp(0..2) #=> 1
 3.clamp(0..2) #=> 2

見ての通り、0..2の範囲に収まるように切り上げ・切り下げをやります。n.clamp(0, 2)で同じことはできていたのですが、どうしてもRangeで書きたいという声があり、対応しました。なおexclusive rangeを与えると例外になります(超えたときの切り下げの意味が定義できないので)。

0.clamp(0...2) #=> ArgumentError (cannot clamp with an exclusive range)

(mame)

Complex#<=>の導入

  • Added Complex#<=>. So 0 <=> 0i will not raise NoMethodError. [Bug #15857]

比較が定義できないことで有名なComplexに比較メソッドが導入されました。

Complex(1, 0) <=> 3 #=> -1

のように、虚数部が0のときは比較できてほしい、というためのもののようです。なお虚数部が0でないComplexの比較はnilになりました。

Complex(0, 1) <=> 1 #=> nil

(mame)

Dir.globDir.[]がNULセパレートパターン非対応に

  • Dir.glob and Dir.[] no longer allow NUL-separated glob pattern. Use Array instead. [Feature #14643]

誰も知らなそうな機能がひっそりと消えました。たとえばファイルfooとbarがあるディレクトリで"f*\0b*"というパターンをDir.globすると

Dir.glob("f*\0b*") #=> ["foo", "bar"]

というように、パターンのORを書くことができました。が、廃止されました。同じことがやりたければ配列が使えます。

Dir.glob(["f*", "b*"]) #=> ["foo", "bar"]

(mame)

CESU-8 というエンコーディングの追加

CESU-8 というエンコーディングが追加されました。

よく知らないのですが、非推奨のエンコーディングだそうなので(UTR #26: Compatibility Encoding Scheme for UTF-16: 8-Bit (CESU-8))、他のシステムが使っているとか、そういうのがなければ関係ないでしょう。

(ko1)

Enumerable#filter_mapの追加

filtermapを同時にやるメソッドが追加されました。

[1, 2, 3].filter_map {|x| x.odd? ? x.to_s : nil } #=> ["1", "3"]

ブロックで変換した結果が偽(falsenil)だったら消されます。両方捨てるべきか、nilだけ捨てるべきかは一長一短で、幾度も議論されましたが、まつもとゆきひろさんの直感によって両方捨てることに。

おおよそ、次と同じ意味です。filterしてmap

[1, 2, 3].filter {|x| x.odd? }.map {|x| x.to_s }  #=> ["1", "3"]

次の例はmapしてfilterのように見えなくもないです。

# 配列の先頭要素を集める、ただし偽は捨てる
[ary1, ary2, ary3].filter_map {|ary| ary.first }

[ary1, ary2, ary3].map {|ary| ary.first }.filter {|elem| elem }

filtermapは組み合わせて使いたいことが多いので、中間配列を作らなくて済む専用メソッドとして導入されました。ケチな話です。

(mame)

Enumerable#tallyの追加

要素の数を数える便利メソッドが導入されました。

["A", "B", "C", "B", "A"].tally
  #=> {"A"=>2, "B"=>2, "C"=>1}

要素をキー、個数を値としたハッシュにして返します。誰しも1度は自分で実装したことがあるのではないでしょうか。

ちなみにtallyとは、線を書きながら数を数える動作を表す単語だそうです(Tally marks - Wikipedia)。日本語だと「正」の字を書いていくやつ。

(mame)

Enumerator.produceの追加

  • Added Enumerator.produce to generate Enumerator from any custom data-transformation. [Feature #14781]

Enumeratorで無限列を作るのに便利なクラスメソッドが追加されました。

naturals = Enumerator.produce(0) {|n| n + 1 } # [0, 1, 2, 3, ...]
p naturals.take(10) #=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

引数が初期値で、それに対して繰り返しブロックの変換を適用することで無限列を作ります。

要望は2.6のころからあったものの、名前がなかなか決まらなかったメソッドでした。Haskellではiterateという名前ですが、Rubyではイテレータという言葉があって紛らわしく、generateはちょっと一般的すぎるのではないか、recurrenceは良くわからない、fromはどうか、などともめて、結局まつもとゆきひろさんの好みでproduceに。

(mame)

Enumerator::Lazy#eagerの追加

  • Added Enumerator::Lazy#eager that generates a non-lazy enumerator from a lazy enumerator. [Feature #15901]

Enumerator::LazyからEnumeratorに変換するメソッドです。これを理解するには、ちょっと長い背景を理解する必要があります。

Rubyには要素の列を表すっぽいデータがArrayEnumeratorEnumerator::Lazyの3種類あります。

Arrayは、すべての要素をメモリ上に並べたデータのクラスで、要素の数だけメモリを消費する特性があります。 一方EnumeratorEnumerator::Lazyは、「次の要素をyieldする計算」を内部表現とするクラスで、同じ列を表現するのでもメモリ消費を回避できます。その代わり、遅い、ランダムアクセスできない、などのデメリットもあり、トレードオフになっています。

EnumeratorEnumeartor::Lazyの違いは何ともややこしいのですが、Enumeratorのメソッドを呼ぶと要素を列挙してArrayにしてしまうことが多いのに対し、Enumerator::Lazyはメソッドを呼んでも#forceメソッドを呼ぶまで要素の列挙が遅延されます。

たとえば、0から10,000までの列に対し、各要素を2倍し、先頭の5要素を取り出したいとします。

# Enumeratorの場合
e = (0..10000).each # Enumerator
p e.map {|n| n * 2 } #=> [0, 2, 4, 6, ...] (Array)

# Enumerator::Lazyの場合
e = (0..10000).each.lazy
p e.map {|n| n * 2 }               #=> #<E::Lazy: #<...>:map>
p e.map {|n| n * 2 }.take(5)       #=> #<E::Lazy: #<...>:take>
p e.map {|n| n * 2 }.take(5).force #=> [0, 2, 4, 6, 8] (Array)

Enumerator#mapはいきなり要素を列挙してArrayを返してしまうので、長さ10,000の配列を作ってしまい、メモリを大量に消費してしまいます。 一方Enumerator::Lazy#mapは、要素の列挙を後回しにしているので、ArrayではなくEnumerator::Lazyを返します。それに対してtake(5)をして最後にforceをすることで、長さ10,000の配列を作らずに先頭5要素を取り出すことができます。

さて、Enumeratorを受け取って、最後に現れた奇数を返すlast_oddメソッドを考えます(あまりいい例ではないですが)。

def last_odd(e)
  e.select {|x| x.odd? }.last
end

このメソッドにEnumerator::Lazyを渡したいとき、どうすればいいでしょうか。そのまま渡すと、selectの結果もEnumerator::Lazyとなり、それにはlastが定義されていないのでエラーになります。forceArrayにしてから渡せば動きますが、メモリを無駄遣いすることになります。Enumerator::LazyからEnumeratorに変換できればいいのですが、実は簡単に変換するAPIがありませんでした。

ということで、今回入ったのがEnumerator::Lazy#eagerです。

e = (0..10000).each.lazy
e = e.map {|n| n * 2 }.take(5).eager #=> [0, 2, 4, 6, 8] 相当の Enumerator(この時点ではまだメモリ確保しない)
p e.map {|n| n + 1 }                 #=> [1, 3, 5, 7, 9] (Array)

これを使ってEnumeratorにすれば、無事last_oddEnumerator::Lazyを渡せます。

e = (0..10000).each.lazy

last_odd(e)       #=> NoMethodError
last_odd(e.force) #=> 9999 (動くけど非効率)
last_odd(e.eager) #=> 9999

いやあ、ややこしいですね。ArrayEnumerator::Lazyだけなら良かったのに、という気もします。

(mame)

Enumerator::Yielder#to_procの追加

  • Added Enumerator::Yielder#to_proc so that a Yielder object can be directly passed to another method as a block argument. [Feature #15618]

Enumerator::Yieldereach&で渡せるようになりました。といってわかる人はほとんどいないと思う。

Enumeratorは作る方法が2つあります。1つは上で示した(0..10000).eachのように、ブロックを省略したeachなどを呼び出す方法、もう1つはEnumerator.new {|y| ... }を使う方法です。

e = Enumerator.new do |y|
  y << 1
  y << 2
  y << 3
end
  
p e.to_a #=> [1, 2, 3]

このyEnumerator::Yielderクラスのオブジェクトです。これにto_procが追加されたので、次のようなことが書けます。

e = Enumerator.new do |y|
  [1, 2, 3].each(&y)
  # 次と同じ意味
  # [1, 2, 3].each {|x| y << x }
end
  
p e.to_a #=> [1, 2, 3]

Enumeratorってどのくらいの人が使いこなしてるのか、気になります。

(mame)

Fiber#raise の追加

  • Added Fiber#raise that behaves like Fiber#resume but raises an exception on the resumed fiber. [Feature #10344]

Fiber#raise というメソッドが追加されました。何をするかというと、まず resume して、その後、resume 先のコンテキストで、例外を発生させます。

f = Fiber.new do
  Fiber.yield
  #=> Fiber#raise によって、ここで例外が発生する
rescue Exception
  p $!
end

f.resume
f.raise

Fiber が worker みたいなことをしているとき、処理を中断させるような例外を出すような用途が考えられます。

worker = Fiber.new do
  loop do
    task = Fiber.yield
    do_task(task)
  end
end

worker.resume # kick
worker.resume 1
worker.resume 2
worker.raise StopIteration # loop から抜ける

が、なんか難しいので(Fiber 作成者の意図しない例外フローを生成する可能性が生じます)、できれば利用を避けた方がいいと思うなあ。resume で明示的に処理をすればいいように思います。

例えば、さっきの例だと、

worker = Fiber.new do
  while task = Fiber.yield
    do_task(task)
  end
end

worker.resume # kick
worker.resume 1
worker.resume 2
worker.resume nil

nil が来たら終わり、と書けば済みます。

(ko1)

File.extnameのコーナーケースが変更

  • File.extname now returns a dot string at a name ending with a dot on non-Windows platforms. [Bug #15267]

ファイル名文字列から拡張子を得るFile.extnameが微妙に変化しました。

File.extname("foo.") #=> ""  in Ruby 2.6
                     #=> "." in Ruby 2.7

なぜかというと、basenameの結果とextnameの結果を結合したときに元に戻るようにするためです。

f = "foo."
b = File.basename(f, ".*") #=> "foo"
e = File.extname(f)        #=> "."
p b + e == f               #=> true

なお、深遠な理由によりWindowsではこれは""のままになりました。よくわからないですがWindowsでは最後がドットで終わるファイル名は無効だからだそうです。

(mame)

FrozenErrorreceiver をサポート

  • Added FrozenError#receiver to return the frozen object that modification was attempted on. To set this object when raising FrozenError in Ruby code, pass it as the second argument to FrozenError.new. [Feature #15751]

freeze されたオブジェクトに対して更新を試みると、FrozenError 例外になります。どのオブジェクトを更新しようとしたか、FrozenError#receiver で知ることができるようになりました。

begin
  ''.freeze << 1
rescue FrozenError => e
  p e.receiver
  #=> ""、つまり Frozen な文字列に対して変更しようとしたことがわかる
end

なお、FrozenError.new(receiver: obj) として、receiver を指定して FrozenError を作れるようになりました。が、まぁ、こんなの作る機会は滅多にないでしょうねぇ。

と、思って rubygems で公開されている Gem のコードを FrozenError.new で検索してみると、122 行みつかりました。意外とあるな。

(ko1)

GC.compact の追加

  • Added GC.compact method for compacting the heap. This function compacts live objects in the heap so that fewer pages may be used, and the heap may be more CoW friendly. [Feature #15626]

Ruby 2.7 の目玉の一つとも言える、ヒープのコンパクション機能です。GC.compact とありますが、GC のたびに行うわけではなく、手動で GC.compact メソッドを実行する度に、コンパクションを行います。

詳細は当該チケットを見て下さい。

GC.compact: コンパクションとは何か

「ヒープのコンパクション」とは何か、ちょっとご紹介します。まず、Ruby のオブジェクトは、ヒープに格納されています。そして、ヒープはページの集合として実装されています。ページは、オブジェクトを格納するスロットの並びです。

さて、オブジェクトを生成するとき、空きスロットを探してそのスロットを新しいオブジェクトとして利用します。GC が発生すると、使っているスロットはそのままに、使っていないオブジェクト(のスロット)を回収し、そのスロットを空きスロットとして確保します。つまり、GC 後は、空きスロットと利用中スロットがそれぞれまばらに存在することになります。

MRI におけるヒープのコンパクションとは、空きスロットのあるページに、生きているオブジェクトを別のページから動かして詰めていく、というものです。結果的に、「空きもあるページ」が沢山あった状態から、「空きのないページ」「空だけのページ」(と、いくらかの「空きもあるページ」)にすることができます。フラグメンテーションの解消ということですね。空きだけのページを解放すれば、メモリ効率がよくなります。

GC.compact: 今回導入された機能

GCアルゴリズムの一つにコピーGCというのがあるのですが、コンパクションを自動的に行うようなアルゴリズムです。存在はもちろん知っていて、なんとかならないかなぁ、とは思っていました。ただ、いろいろな理由(主に性能的な理由)から、コピーGCのような、毎回コンパクションを行うような GC を導入するのは難しいなあと思っていたんですが、今回導入された GC.compactは「人間が明示的に指示する」という発想の転換で、見事に実現されました。

なお、技術的には、MRIでは、「オブジェクトを動かす」ということが出来ないオブジェクトがいくつかあります(歴史的経緯です)。そのため、それらはそのまま残して、動かせる奴だけ動かす、というアルゴリズムになっています。mostly compaction algorithm と呼ばれます。

GC.compactのための変更規模は大変大きく、もしかしたらまだ問題が残っているかも知れません。ご利用する際は、もしかしたら問題あるかなー、という覚悟を持ってご利用下さい。多分、あまり自分で呼ぶようなものでもないと思います(フレームワークが呼んでくれるかも?)。何か問題を見つけたら教えて下さい。

(ko1)

IO#set_encoding_by_bom の追加

  • Added IO#set_encoding_by_bom to check the BOM and set the external encoding. [Bug #15210]

Unicode データの最初に、BOMがついていることがあります。IOにBOMがついていれば、それにあわせた外部エンコーディングを設定し、BOMを読み捨てる IO#set_encoding_by_bom が追加されました。

なお、IO は binmode で開いておく必要があります。

io = open("with_bom", 'rb')
p io.tell                #=> 0
p io.external_encoding   #=> #<Encoding:ASCII-8BIT>
p io.set_encoding_by_bom #=> #<Encoding:UTF-8>
p io.tell                #=> 3 (読み捨てられた)
p io.external_encoding   #=> #<Encoding:UTF-8>

(ko1)

Integer#[]がRangeをサポート

nビット目の数字を0か1を取り出すInteger#[]というメソッドがあるのですが、これを範囲に対応させました。

# 2ビット目から5ビット目までの4ビットを取り出す
0b01001101[2, 4]  #=> 0b0011
0b01001100[2..5]  #=> 0b0011
0b01001100[2...6] #=> 0b0011
#   ^^^^ この位置の4ビットを取り出す

# ビット演算で同じことをやるなら
(0b01001100 >> 2) & ((1 << 4) - 1)

ビット演算はわりと複雑になるので、そういうことをやりたいときには便利なんじゃないでしょうか。

(mame)

Method#inspect の結果がリッチに

Method#inspect の表記がリッチになりました。

具体的には、

  • (1) パラメータの情報
  • (2) 定義された場所の情報

の二つの情報が入りました。

def foo(a, b=1, *r, p1, k1: 1, rk:); end
p method(:foo)
#=> #<Method: main.foo(a, b=..., *r, p1, rk:, k1: ...) t.rb:2>

このとき、(1) は (a, b=..., *r, p1, k1: ...) で、(2) はt.rb:2 です。

このメソッドなんだっけ? という時、とりあえず inspect すれば良い、と言う意味で、便利になったんじゃないかと思います。pry 上だと $ で、色々情報取れるそうですが。

余談ですが、最初、(1)と(2)の間は、場所を表す "at" の意味で、@ で区切っていました。が、端末上でファイル名をコピペするとき、スペース区切りのほうが、ダブルクリックだけで済むから楽、という理由でスペース区切りにしました。なるほどなぁ。

(ko1)

Module#const_source_location の追加

  • Added Module#const_source_location to retrieve the location where a constant is defined. [Feature #10771]

定数の定義位置を返す Module#const_source_location が追加されました。位置は [file_name, line_number] の配列で返します。

class C
  class D
  end
end

p Object.const_source_location('C')
#=> ["t.rb", 1]
p Object.const_source_location('C::D')
#=> ["t.rb", 2]

(ko1)

Module#autoload?inherit オプションに対応

  • Module#autoload? now takes an inherit optional argument, like as Module#const_defined?. [Feature #15777]

Module#autoload? に、継承元のクラスの autoload の状況を見るかどうかを指示する inherit オプションが追加されました。デフォルトは true です。

サンプルを RDoc から引用します。

   class A
     autoload :CONST, "const.rb"
   end

   class B < A
   end

   B.autoload?(:CONST)          #=> "const.rb", found in A (ancestor)
   B.autoload?(:CONST, false)   #=> nil, not found in B itself

(ko1)

いろいろ、Frozen な文字列に

  • Module#name now always returns a frozen String. The returned String is always the same for a given Module. This change is experimental. [Feature #16150]

  • NilClass#to_s, TrueClass#to_s and FalseClass#to_s now always return a frozen String. The returned String is always the same for each of these values. This change is experimental. [Feature #16150]

モジュール名を返す Module#nameや、true.to_s などの結果が、一意な frozen な文字列になりました。以前は、毎回書き換え可能なアロケーションしてたんですよね。

ちなみに、Symbol#to_s の結果も frozen にしようって実験が行われたそうですが、そっちはうまくいかなくて revert されました。

(ko1)

ObjectSpace::WeakMap#[]=がシンボルなども保持できるように

  • ObjectSpace::WeakMap#[]= now accepts special objects as either key or values. [Feature #16035]

ObjectSpace::WeakMapという、直接使うことが推奨されていないクラスの話なので、ここは読まなくていいです。読むな。

WeakMapはハッシュみたいなオブジェクトですが、キーや値がGCに回収されたら中身が勝手に消えます。

o = ObjectSpace::WeakMap.new
o["key"] = "value"
p o.size #=> 1

GC.start

p o.size #=> 0

消えることはGCに依存していて保証はされてないので、イメージです。あと# frozen-string-literal: trueだと消えないので注意。

WeakMapは内部的に、キーや値にファイナライザを設定するので、シンボルや数値のようにファイナライザが設定できないオブジェクトを持たせることはできませんでした。

が、今回それを許すようにしました。WeakMapをキャッシュ的に使う上でこの制限が面倒だったから、ということですが、そもそも直接利用を推奨されてないクラスなので、何が起きるやらわかりません。生暖かく見守っていきましょう。

(mame)

$LOAD_PATH.resolve_feature_pathの追加

Ruby 2.6で、requireを呼んだときに読み込まれるファイルを特定するRubyVM.resolve_feature_pathというメソッドが入りましたが、これが$LOAD_PATHの特異メソッドに移動しました。

RubyVMはいわゆるMRI(Matz Ruby Implementation)特有のものを置くところなのですが、resolve_feature_pathは他の実装でも使いたい可能性がある、ということで、外に移そうということになりました。が、多くの人が使うメソッドでもないのでKernelに置くほどのものでもなく、行き先に困り、議論の末、$LOAD_PATHの特異メソッドという大変微妙な位置になりました。

(mame)

Unicodeのバージョンが上がった

対応するUnicodeのバージョンが11から12.1.0に上がりました。

Unicode 12にはたとえば、小さい「ゐ」(U+1B150)が入ったそうです。遠藤の環境では表示できませんでしたが。これはinsmallkanaextensionという文字プロパティを持ってるそうなので、正規表現でこれにマッチできます。

p /\p{insmallkanaextension}/ =~ "\u{1b150}" #=> 0

非常に地道ですが、普及したころには恩恵を受ける人もいるのではないでしょうか。

(mame)

Symbol#start_with?Symbol#end_with? の追加

タイトルの通りで、内容もメソッド名見ればわかりますよね。Stringにある二つのメソッドが Symbol に追加されました。

StringSymbolは、どこで線が引かれるんですかねえ。この辺、歴史のある課題です。Symbol 原理主義者はまったく別物であると主張し、String 過激派は同じにしろと主張しています。どちらかというと、String 過激派のほうに流れていっているような気がしますね。

(ko1)

Time#ceilTime#floor メソッドの追加

Time オブジェクトは、実は秒より高い精度の時間(ナノ秒)を持つことができます。

p Time.now.nsec #=> 532872900

Time#round という、メソッドは、時間を秒で丸める処理をしますが、同じように、floor(切り上げる)と ceil(切り下げる)が追加されました。

(ko1)

Time#inspectTime#to_s と別になり、inspect はナノ秒まで出力

  • Time#inspect is separated from Time#to_s and it shows its sub second. [Feature #15958]

で、Ruby 2.6 までは、to_sinspectは秒より細かい情報を切り捨てて表示していたので、比較したら異なる値のはずが、p などで見ると同じ、という現象がありました。

そこで、Time#to_sTime#inpsect を分離し、Time#inpsect はナノセカンドまで(もしあれば)返すようになりました。Time#to_s が変更されなかったのは、互換性維持のためだそうです。

t = Time.now
p [t.to_s, t.floor.to_s]
#=> ["2019-12-21 04:27:05 +0900",
#    "2019-12-21 04:27:05 +0900"]
#   .to_s だと同じに見える

p [t.inspect, t.floor.inspect]
#=> ["2019-12-21 04:27:05.3067204 +0900",
#    "2019-12-21 04:27:05 +0900"]
#   .inspect だと別物だとわかる

p t.round == t #=> false

(ko1)

UnboundMethod#bind_callの追加

Ruby上級者向けの機能です。普通のプログラムでは使わないでください。

クラスを継承してメソッドをオーバーライドすると、新しいメソッドが呼ばれるようになります。

class Foo
  def foo
    "foo"
  end
end
class Bar < Foo
  def foo # override
    "bar"
  end
end

obj = Bar.new
p obj.foo #=> "bar"

当たり前ですね。しかし、黒魔術的なケースでごくまれに、オーバーライドされる前のメソッドを呼び出したい、という要求があります。このとき、UnboundMethod#bindMethod#callを組み合わせる悪魔イディオムを使う人がいます。

p Foo.instance_method(:foo).bind(obj).call #=> "foo"

Fooのインスタンスメソッドオブジェクト(UnboundMethod)を取り出し、それをターゲットオブジェクトにbindして、callするので、オーバーライドされる前のメソッドが呼び出せてしまいます。しかし、bindしてcallするのは結構重たい演算なので、まとめてやれば多少速くなる、ということでbind_callが導入されました。

p Foo.instance_method(:foo).bind_call(obj) #=> "foo"

なお、この悪魔イディオムが必要になるのは、ppやランタイムモニタのように、どんなオブジェクトが来るのかまったく予想できないというケースです。普通のプログラムでは決して使わないでください。

(mame)

警告のカテゴリ別フィルタの追加(Warning.[], Warning.[]= の追加)

  • Added Warning.[] and Warning.[]= to manage emit/suppress of some categories of warnings. [Feature #16345]

Warning[category] = true or false とすることで、category に属する警告を、有効 or 無効にすることができるようになりました。

Ruby 2.7 では互換性が変更するところが多く、まとめて警告を消す方法が議論されました。ただ、すべての警告を消してしまうと、興味があるかもしれない警告も一緒に抑制してしまいます。

そこで、あるカテゴリの警告のみ有効・無効を制御したい、ということで、Warning.[] および Warning.[]= が追加されました。現在カテゴリは :deprecated(非対応警告)、:experimental (実験機能警告)の二つだけしかありません。今後、整理されていくのではないでしょうか(でもなぁ、誰がやるのかなぁ)。

Warning[:deprecated] = false

def foo
  proc # デフォルトでは deprecated 警告が出るが、その警告を抑制した
end
foo{}

(ko1)

標準添付ライブラリのアップデート

ライブラリも、いろいろアップデートしました。NEWS にいくつか載っていますが、興味のあるところだけご紹介します。

エスケープがあるとき、CGI.escapeHTML が2~5倍高速化

CGI.escapeHTMLが速くなったそうです。

(ko1)

IRBの刷新

irbが刷新され、次のような機能が搭載されました。

  • 複数行編集
  • オートインデント
  • メソッド名補完
  • ドキュメント(rdoc)検索
  • シンタックスハイライト

文章でごちゃごちゃ説明するのは無粋なので、ぜひ実際に体験してください。いますぐ2.7をインストール。

……がすぐに出来ない人は、実は2.6でも動くので、gem install irbしてみてください。……それも難しい人は、リリースアナウンスに載っている動画を見て雰囲気を感じてください。

Rubyの対話的環境というとすっかりpry一色でしたが、irbが追いつき追い越す面も出てきたので、競争で便利になるといいですね。pryも複数行編集のサポートを考えているという噂です。

IRBの刷新: すごさと注意点

ターミナルというのは、タイプライタの時代から改良が続けられてきた超絶レガシーで、意外と大変です。色を付けるくらいなら簡単なのですが、複数行編集・オートインデント・補完となると、ちょっとしたエディタになります。それがLinuxだけでなくWindowsのコンソールでも動きます。JRubyでも動きます。ncursesみたいなライブラリはいろいろ制約があって使えないので全部自力でやっていて、簡易screenやtmuxくらいの複雑さになってます。それがちゃんと動いているのですごい。

一方で、今回MRIのパッケージに含まれ、はじめて広く使われることになります。先に述べた通りターミナルというのは超絶レガシーで、ターミナルによって挙動の違いがあったり、コーナーケースがあったりします。この改良を成し遂げた糸柳さんは2018年ごろから開発を始めたようなので、様々な環境・使い方は経験できておらず、枯れているとは言えません。ぜひ使ってみて、おかしな挙動を見つけたらフィードバックしてください。「とりあえず今動かなくて困る!」というときは、irb --legacyというオプション付きで起動すれば、おおよそ従来バージョンで動きます。

(mame)

OptionParserでオプションをtypoしたらdid you meanが表示されるように

Rubyにはしばらく前からdid_you_mean gemが組み込まれていて、メソッド名や定数名のtypoで修正候補を出してくれますが、それをOptionParserにも組み込んでみました。

$ ruby test.rb --hepl
Traceback (most recent call last):
tt:6:in `<main>': invalid option: --hepl (OptionParser::InvalidOption)
Did you mean?  help

--helpを打ち間違えて--heplとしてしまっていますが、"Did you mean? help"というふうに修正候補を挙げてくれます。

test.rbは普通にOptionParserを使っているだけです。

require 'optparse'
OptionParser.new do |opts|
  opts.on("-f", "--foo", "foo") {|v| }
  opts.on("-b", "--bar", "bar") {|v| }
  opts.on("-c", "--baz", "baz") {|v| }
end.parse!

(mame)

非互換

  • The following libraries are no longer bundled gems. Install corresponding gems to use these features.

    • CMath (cmath gem)
    • Scanf (scanf gem)
    • Shell (shell gem)
    • Synchronizer (sync gem)
    • ThreadsWait (thwait gem)
    • E2MM (e2mmap gem)

これらのライブラリは bundled gem(つまり、Ruby のインストール時に勝手にインストールされる gem)ではなくなりました。もし必要なら、Gemfile などに入れるようにして下さい。

(ko1)

Proc#to_s のフォーマットが変わった

Proc#to_sProc#inspectもaliasなので同じ)は、ファイル名と行番号を含んだ文字列を返します(Proc を生成した場所です)。2.6 では、...@file.rb:123 だったのが、... file.rb:123 のように、@が空白に変わりました。

つまり、こんな感じです。

p proc{}.to_s
#=>
# Ruby 2.6
# "#<Proc:0x0000024cc385c3e0@t.rb:1>"
# Ruby 2.7
# "#<Proc:0x0000024cc385c3e0 t.rb:1>"

Method#to_sにあわせた変更ですね。

たいした違いじゃないんですが、minitest のテストだったかで、正規表現を使ってファイル名を取り出しているコードがあって、失敗しちゃってました。念のため非互換のところに入れています。

(ko1)

ライブラリの非互換

Gem化

  • Promote stdlib to default gems
    • The following default gems was published at rubygems.org
      • benchmark
      • cgi
      • delegate
      • getoptlong
      • net-pop
      • net-smtp
      • open3
      • pstore
      • singleton

これらのライブラリはデフォルトgemになりました。rubygems.orgでも公開されます。

  • The following default gems only promoted ruby-core, Not yet published at rubygems.org.
    • monitor
    • observer
    • timeout
    • tracer
    • uri
    • yaml

これらのライブラリはデフォルトgemになりましたが、rubygems.org ではまだ公開されていません(調整中だそうです)。

  • The did_you_mean gem has been promoted up to a default gem from a bundled gem

did_you_mean gem が、bundled gem から default gem になりました。

(ko1)

Pathname()

  • Kernel#Pathname when called with a Pathname argument now returns the argument instead of creating a new Pathname. This is more similar to other Kernel methods, but can break code that modifies the return value and expects the argument not to be modified.

Pathname(obj)で、objPathanmeだったとき、新しいPathnameを返すのでは無く、obj自身が返るようになりました。

p1 = Pathname('/foo/bar')
p2 = Pathname(p1)
p p1.equal?(p2)
#=> Ruby 2.6: false
#   Ruby 2.7: true
#=> 

(ko1)

profile.rb, Profiler__

  • Removed from standard library. No one maintains it from Ruby 2.0.0.

標準ライブラリから外されました。誰もメンテナンスしていないからとのことです。Gem で提供予定ですが、調整中とのことです。

(ko1)

コマンドラインオプションの変更

-W:(no-)category オプションの追加

Warning[category] = true or false の機能を、コマンドラインでも指定できるようになりました。

  • 警告を有効にする: -W:category
  • 警告を無効にする: -W:no-category

と指定します。Warining[category] と同様、現在カテゴリは deprecatedexperimental の二つです。

利用例:

    # deprecation warning
    $ ruby -e '$; = ""'
    -e:1: warning: `$;' is deprecated

    # suppress the deprecation warning
    $ ruby -W:no-deprecated -e '$; = //'

    # works with RUBYOPT environment variable
    $ RUBYOPT=-W:no-deprecated ruby -e '$; = //'

    # experimental feature warning
    $ ruby -e '0 in a'
    -e:1: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

    # suppress experimental feature warning
    $ ruby -W:no-experimental -e '0 in a'

    # suppress both by using RUBYOPT
    $ RUBYOPT='-W:no-deprecated -W:no-experimental' ruby -e '($; = "") in a'

(ko1)

C API の変更

  • Many *_kw functions have been added for setting whether the final argument being passed should be treated as keywords. You may need to switch to these functions to avoid keyword argument separation warnings, and to ensure correct behavior in Ruby 3.

Ruby 3 でキーワード分離を行うために、_kw で終わる関数名の関数が追加されました。

  • The : character in rb_scan_args format string is now treated as keyword arguments. Passing a positional hash instead of keyword arguments will emit a deprecation warning.

rb_scan_args()のフォーマット文字列 : が最後のオプショナルハッシュではなく、最新のキーワード引数の意味にあわせました。

関数ポインタを受け取るとき、その引数がよくわからん、というのを示す ANYARGS という機能が使えなくなりました。ちゃんと関数ポインタの型を書きましょうね、という話です。

(ko1)

性能向上

Ruby 2.7向けに行われた性能向上についてです。

Fiberとスレッドの実装向上

  • Allow selecting different coroutine implementation by using --with-coroutine=, e.g.
         ./configure --with-coroutine=ucontext
         ./configure --with-coroutine=copy

configureで、Fiberの実装方法を選べるようになりました。が、まぁ、気にする必要は無いでしょう(デフォルトでよいでしょう)。

  • Replace previous stack cache with fiber pool cache. The fiber pool allocates many stacks in a single memory region. Stack allocation becomes O(log N) and fiber creation is amortized O(1). Around 10x performance improvement was measured in micro-benchmarks. https://github.com/ruby/ruby/pull/2224

Fiber のために割り当てられるスタックの戦略を色々と改善して、Fiberの生成とかが10倍くらい速くなりました。やった!

環境によるんですが、mmapでドーンと大きな領域を確保しておき、それを分割して使っていくという戦略になります。

  • VM stack memory allocation is now combined with native thread stack, improving thread allocation performance and reducing allocation related failures. ~10x performance improvement was measured in micro-benchmarks.

同じような話なんですが、VMスタックをマシンスタックから alloca で取得することにより、VMスタック割り当て時間が随分へりました。これも 10 倍くらい速くなったそうです。... 何と比べてだろう?

(ko1)

realpath(3)の利用

  • File.realpath now uses realpath(3) on many platforms, which can significantly improve performance.

使えるならrealpath(3)を利用することで性能が向上したそうです(よく知らない)。

(ko1)

Hash のデータ構造の改善

小さいハッシュ(具体的には 1~8 要素)が必要とするメモリが、192バイトだったのが128バイトになりました(64ビット環境)。

キーと値のペアごとにハッシュ値を保存していたのを、1バイトだけ保存するように変えることで実現しています。効果あるといいなぁ。

(ko1)

MonitorのC実装化による高速化

Monitorクラス(MonitorMixinモジュール)はRubyで書かれていたのですが、handle_interruptという機能を使って実装していると、無視出来ないオーバヘッドとなってしまったそうです。とくに、Ruby 2.6 で、適切な実装にするために追加してしたコードが遅かったとか。

そこで、C で書き直すことにより、以前よりもそこそこ速くなりました。

(ko1)

インラインメソッドキャッシュの改善

メソッド呼び出しを行うところに前回のメソッド探索結果をキャッシュしておくインラインメソッドキャッシュにおいて、クラスは一致しないが同じメソッドを参照する場合、それらのクラスもキャッシュのキーとして保存することで、メソッドキャッシュがより効くようになりました。

discourse ベンチマークという、Rails アプリを対象にした実験において、インラインメソッドキャッシュのヒット率が89%から94%に向上したそうです。Ruby でメソッド呼び出しは大量に行われる処理なので、ここが速くなるのは大変重要なわけです。

(ko1)

JITの改善

  • JIT-ed code is recompiled to less-optimized code when an optimization assumption is invalidated.

高度な最適化のために、いくつか前提とする条件があるのですが、その条件が外れたとき、その条件を緩和して、その最適化を行わないバージョンに再度コンパイルするようになりました。

  • Method inlining is performed when a method is considered as pure. This optimization is still experimental and many methods are NOT considered as pure yet.

「ピュア」なメソッドをインライン化する実験的な機能が実装されました。「ピュア」の定義は面倒なので省略しますが、ほとんどの場合、ピュアじゃないと思います。

  • Default value of --jit-max-cache is changed from 1,000 to 100

--jit-max-cache というパラメータのデフォルトが1,000から100になりました。これは何かというと、何個のメソッドをJITしたままにしておくか、という数になります。

  • Default value of --jit-min-calls is changed from 5 to 10,000

--jit-min-callsというパラメータのデフォルト値が、5から1万になりました。このパラメータは、何回呼ばれたらJITコンパイルするか、そのしきい値になります。

(ko1)

コンパイル済み命令列のサイズ削減

  • RubyVM::InstructionSequence#to_binary method generate compiled binary. The binary size is reduced. [Feature #16163]

RubyVM::InstructionSequence#to_binary というメソッドで、VMが実行する命令列、いわゆるバイトコードをバイナリに変換し、出力することができます。これらのバイナリは、Bootsnap などで利用されており、Rubyアプリケーションの起動の高速化に利用されています。

この出力は、非常に無駄が多いフォーマットだったので、クックパッドにインターンに来て頂いた永山さんに、仕様を検討してもらい、スリムにして出力サイズを削減してもらいました。詳細はRuby中間表現のバイナリ出力を改善する - クックパッド開発者ブログをご参照下さい。

(ko1)

その他

そのほかの変更です。

IA64 のサポートを中止

  • Support for IA64 architecture has been removed. Hardware for testing was difficult to find, native fiber code is difficult to implement, and it added non-trivial complexity to the interpreter. [Feature #15894]

Itaniumの製造って終了したらしいですね。というわけで、もうサポートやめようか、ということになりました。結構特殊な処理が入ってたんですよね。

(ko1)

C99の利用

MRIの実装を、C89 ではなく、C99 を用いて書くことができるようになりました(いくつか制限があります)。// コメントが書けるようになった! でも、もう20年前の仕様じゃん。

(ko1)

Git化

  • Ruby's upstream repository is changed from Subversion to Git.

ソースコードが Git で管理されるようになりました。Github で全部管理するんじゃなくて、Git リポジトリが別にあり、そこと Github のリポジトリが良い感じに同期しているような構成になっています。

  • RUBY_REVISION class is changed from Integer to String.

Git 化にともない、今まで Subversion のリビジョン(数値)だった RUBY_REVISION が、Git のコミットハッシュになりました。

p RUBY_REVISION
#=> "fbe229906b6e55c2e7bb1e68452d5c225503b9ca"
  • RUBY_DESCRIPTION includes Git revision instead of Subversion's one.

同じく、Subversion のリビジョンを含んでいた RUBY_DESCRIPTION がコミットハッシュを含むようになりました。

p RUBY_DESCRIPTION
#=> "ruby 2.7.0dev (2019-12-17T04:15:38Z master fbe229906b) [x64-mswin64_140]"
# 開発版の表記なので、リリース版は多分また違うんだと思います。

(ko1)

組込クラスを Ruby で書くためのサポート

  • Support built-in methods in Ruby with __builtin_ syntax. [Feature #16254] Some methods are defined in *.rb (such as trace_point.rb). For example, it is easy to define a method which accepts keyword arguments.

RubyKaigi 2019 で私が発表した内容(詳細は RubyKaigi 2019: Write a Ruby interpreter in Ruby for Ruby 3 - クックパッド開発者ブログ)の話です。

簡単にまとめると、現在は Arrayなどの組込クラスはCで記述するしかなかったのが、RubyとCを簡単に組み合わせることで書ける、というものです。

現在は、いくつかのクラス(例えば、trace_point.rbというファイルで TracePoint の定義が書いてあります)でだけ利用していますが、今後はこちらに寄せていきたいと思っています(:contribution_chance:)。

この仕組みについての詳細は、また今度まとめたいと思います。

なお、この仕組みを入れる前提として、前述のコンパイル済み命令列のサイズ削減が役に立ちました。

(ko1)

おわりに

Ruby 2.7 も、様々な変更がありました。ぜひ、使ってみてください。

来年はついに Ruby 3 のリリースが予定されています。楽しみですね。ちゃんと出るといいなぁ。

では、メリークリスマス!