技術部の笹田(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 の意味などについての変更です。
パターンマッチ
- Pattern matching is introduced as an experimental feature. [Feature #14912]
データ構造をいい感じにチェック・分解する機能が入りました。
パターンマッチとは
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 でパターンマッチ: 既存のRuby構文で模倣する試み
- Pattern Matching in Ruby:
%p{}
を導入する提案と試作 - Feature #14709 - Proper pattern matching: 上の試作を受けたチケット
- Qo - Query Object - Pattern matching and fluent querying in 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
andKernel#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#split
やArray#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 が戻ってきた
- The flip-flop syntax deprecation is reverted. [Feature #5400]
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.
を付けてもプライベートメソッドが呼べるようになった
- Calling a private method with a literal
self
as the receiver is now allowed. [Feature #11297] [Feature #16123]
プライベートメソッドはレシーバを付けて呼び出すことができませんでした。すなわち、プライベートメソッド 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)
引数を転送する記法 (...)
- Argument forwarding by
(...)
is introduced. [Feature #16253]
受け取った引数をそのまま他のメソッドに転送するための記法が導入されました。
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
フラグ)、system
や open
といった、危ない操作っぽいものをしようとしたら、インタプリタが「危ないからやめて!」と止めてくれる(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
の導入
- Added Array#intersection. [Feature #16155]
配列の共通の要素だけを取り出す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 thanEnumerable#minmax
. It returns a maximum that now corresponds toRange#max
. [Bug #15807]
同じように、Range#minmax
も別途用意されました。なお、最大値を算出するアルゴリズムが、Enumerable#max
ではなく、Range#max
を用いるため、もしかしたら非互換が出るかも知れません。
(ko1)
Comparable#clamp
がRange引数に対応
- Comparable#clamp now accepts a Range argument. [Feature #14784]
-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#<=>
. So0 <=> 0i
will not raiseNoMethodError
. [Bug #15857]
比較が定義できないことで有名なComplexに比較メソッドが導入されました。
Complex(1, 0) <=> 3 #=> -1
のように、虚数部が0のときは比較できてほしい、というためのもののようです。なお虚数部が0でないComplexの比較はnil
になりました。
Complex(0, 1) <=> 1 #=> nil
(mame)
Dir.glob
やDir.[]
がNULセパレートパターン非対応に
Dir.glob
andDir.[]
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 というエンコーディングの追加
- Added new encoding CESU-8 [Feature #15931]
CESU-8 というエンコーディングが追加されました。
よく知らないのですが、非推奨のエンコーディングだそうなので(UTR #26: Compatibility Encoding Scheme for UTF-16: 8-Bit (CESU-8))、他のシステムが使っているとか、そういうのがなければ関係ないでしょう。
(ko1)
Enumerable#filter_map
の追加
- Added Enumerable#filter_map. [Feature #15323]
filter
とmap
を同時にやるメソッドが追加されました。
[1, 2, 3].filter_map {|x| x.odd? ? x.to_s : nil } #=> ["1", "3"]
ブロックで変換した結果が偽(false
かnil
)だったら消されます。両方捨てるべきか、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 }
filter
とmap
は組み合わせて使いたいことが多いので、中間配列を作らなくて済む専用メソッドとして導入されました。ケチな話です。
(mame)
Enumerable#tally
の追加
- Added Enumerable#tally. [Feature #11076]
要素の数を数える便利メソッドが導入されました。
["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には要素の列を表すっぽいデータがArray
、Enumerator
、Enumerator::Lazy
の3種類あります。
Array
は、すべての要素をメモリ上に並べたデータのクラスで、要素の数だけメモリを消費する特性があります。
一方Enumerator
とEnumerator::Lazy
は、「次の要素をyieldする計算」を内部表現とするクラスで、同じ列を表現するのでもメモリ消費を回避できます。その代わり、遅い、ランダムアクセスできない、などのデメリットもあり、トレードオフになっています。
Enumerator
とEnumeartor::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
が定義されていないのでエラーになります。force
でArray
にしてから渡せば動きますが、メモリを無駄遣いすることになります。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_odd
にEnumerator::Lazy
を渡せます。
e = (0..10000).each.lazy last_odd(e) #=> NoMethodError last_odd(e.force) #=> 9999 (動くけど非効率) last_odd(e.eager) #=> 9999
いやあ、ややこしいですね。Array
とEnumerator::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::Yielder
をeach
に&
で渡せるようになりました。といってわかる人はほとんどいないと思う。
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]
このy
がEnumerator::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 likeFiber#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)
FrozenError
が receiver
をサポート
- Added
FrozenError#receiver
to return the frozen object that modification was attempted on. To set this object when raisingFrozenError
in Ruby code, pass it as the second argument toFrozenError.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をサポート
Integer#[]
now supports range operation. [Feature #8842]
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
shows much information. [Feature #14145]
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 aninherit
optional argument, like asModule#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
andFalseClass#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のバージョンが上がった
Update Unicode version to 12.1.0, adding support for U+32FF SQUARE ERA NAME REIWA. [Feature #15195]
Update Unicode Emoji version to 12.1 [Feature #16272]
対応するUnicodeのバージョンが11から12.1.0に上がりました。
Unicode 12にはたとえば、小さい「ゐ」(U+1B150)が入ったそうです。遠藤の環境では表示できませんでしたが。これはinsmallkanaextension
という文字プロパティを持ってるそうなので、正規表現でこれにマッチできます。
p /\p{insmallkanaextension}/ =~ "\u{1b150}" #=> 0
非常に地道ですが、普及したころには恩恵を受ける人もいるのではないでしょうか。
(mame)
Symbol#start_with?
、Symbol#end_with?
の追加
- Added Symbol#start_with? and Symbol#end_with? method. [Feature #16348]
タイトルの通りで、内容もメソッド名見ればわかりますよね。String
にある二つのメソッドが Symbol
に追加されました。
String
とSymbol
は、どこで線が引かれるんですかねえ。この辺、歴史のある課題です。Symbol 原理主義者はまったく別物であると主張し、String 過激派は同じにしろと主張しています。どちらかというと、String 過激派のほうに流れていっているような気がしますね。
(ko1)
Time#ceil
、Time#floor
メソッドの追加
- Added Time#ceil method. [Feature #15772]
- Added Time#floor method. [Feature #15653]
Time
オブジェクトは、実は秒より高い精度の時間(ナノ秒)を持つことができます。
p Time.now.nsec #=> 532872900
Time#round
という、メソッドは、時間を秒で丸める処理をしますが、同じように、floor
(切り上げる)と ceil
(切り下げる)が追加されました。
(ko1)
Time#inspect
が Time#to_s
と別になり、inspect
はナノ秒まで出力
Time#inspect
is separated fromTime#to_s
and it shows its sub second. [Feature #15958]
で、Ruby 2.6 までは、to_s
やinspect
は秒より細かい情報を切り捨てて表示していたので、比較したら異なる値のはずが、p
などで見ると同じ、という現象がありました。
そこで、Time#to_s
と Time#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
の追加
- Added UnboundMethod#bind_call method. [Feature #15955]
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#bind
とMethod#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.[]
andWarning.[]=
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 becomes 2~5x faster when there's at least one escaped character. https://github.com/ruby/ruby/pull/2226
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_s format was changed. [Feature #16101]
Proc#to_s
(Proc#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
- The following default gems was published at rubygems.org
これらのライブラリはデフォルト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 newPathname
. 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)
で、obj
がPathanme
だったとき、新しい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
オプションの追加
-W
option has been extended with a following:
, to manage categorized warnings. [Feature #16345] [Feature #16420]
Warning[category] = true or false
の機能を、コマンドラインでも指定できるようになりました。
- 警告を有効にする:
-W:category
- 警告を無効にする:
-W:no-category
と指定します。Warining[category]
と同様、現在カテゴリは deprecated
と experimental
の二つです。
利用例:
# 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()
のフォーマット文字列 :
が最後のオプショナルハッシュではなく、最新のキーワード引数の意味にあわせました。
- C API declarations with
ANYARGS
are changed not to useANYARGS
https://github.com/ruby/ruby/pull/2404
関数ポインタを受け取るとき、その引数がよくわからん、というのを示す 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 のデータ構造の改善
- Change data structure of small Hash objects. [Feature #15602]
小さいハッシュ(具体的には 1~8 要素)が必要とするメモリが、192バイトだったのが128バイトになりました(64ビット環境)。
キーと値のペアごとにハッシュ値を保存していたのを、1バイトだけ保存するように変えることで実現しています。効果あるといいなぁ。
(ko1)
Monitor
のC実装化による高速化
Monitor
class is written in C-extension. [Feature #16255]
Monitor
クラス(MonitorMixin
モジュール)はRubyで書かれていたのですが、handle_interrupt
という機能を使って実装していると、無視出来ないオーバヘッドとなってしまったそうです。とくに、Ruby 2.6 で、適切な実装にするために追加してしたコードが遅かったとか。
そこで、C で書き直すことにより、以前よりもそこそこ速くなりました。
(ko1)
インラインメソッドキャッシュの改善
- Per-call-site method cache, which has been there since around 1.9, was improved: cache hit rate raised from 89% to 94%. See https://github.com/ruby/ruby/pull/2583
メソッド呼び出しを行うところに前回のメソッド探索結果をキャッシュしておくインラインメソッドキャッシュにおいて、クラスは一致しないが同じメソッドを参照する場合、それらのクラスもキャッシュのキーとして保存することで、メソッドキャッシュがより効くようになりました。
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の利用
- Require compilers to support C99 [Misc #15347]
- Details of our dialect: https://bugs.ruby lang.org/projects/ruby-trunk/wiki/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 のリリースが予定されています。楽しみですね。ちゃんと出るといいなぁ。
では、メリークリスマス!