初参加&初登壇の RubyKaigi Takeout 2021 で Ruby のマクロの話をしてきたよレポート

書こう書こうと思って気がついたら1ヶ月立っていましたこんにちは。
(去年を除いて)今回が RubyKaigi 初参加&初登壇ということでいろいろと書き残しておこうと思います。
ちなみに今週末に以下のようなイベントで雑に話すので興味がある方がぜひぜひ参加してみてくださいー。

ちなみに『Ruby のマクロ』という主語がクソデカなんですがあくまでも『この登壇では』という前提になります。

Use Macro all the time ~ マクロを使いまくろ ~

動画

www.youtube.com

補足

  • RubyVM::AST はバージョンごとに非互換
    • Ruby 2.6 -> 2.7 で :ARRAY -> :LIST に命令が変わったのがでかい
    • 現時点で存在してるバージョンはすべて対応している
  • Ruby 3.1dev では RubyVM::AST::Node から元のソースコードを取得する機能が入った
    • ただし『配列形式の AST』は依然として復元することができない
    • それはそう
src = <<~EOS
if hoge
  puts hoge + foo
end
EOS

# keep_script_lines を true にすると
node = RubyVM::AbstractSyntaxTree.parse(src, keep_script_lines: true)

# #source メソッドでコードを取得できるようになる
puts node.source
# => if hoge
#      puts hoge + foo
#    end

Ruby でマクロを実装しようと思った経緯

元々は1年ぐらい前に『ブロック内のコードをテキストで取得したい』ということをやりたくて最初はこんな感じで iseq からブロックが定義されているファイルや位置情報を取得してきて実際にそのファイルからソースコードを取得する実装を書いていました。
最初はこれを使っていろいろと遊んでたんですがファイルを読み込む必要がある都合上どうしてもパフォーマンスが気になっていました。
そこで RubyVM::AbstractSyntaxTree.of を利用してブロックから AST を取得し、それを元にして Ruby のコードを復元しよう!としたのが rensei-gem をつくりはじめたきっかけですね。
しばらくは『AST から Ruby のコードに変換する』を目的ととして rensei-gem をつくっていたんですが『あれ、AST を好きに変えたら Ruby のコードの意味を変えれるのでは…?』『これってマクロでは…?』などと考えるようになりました。
元々は Rust のようなマクロが Ruby にもほしいな〜〜〜と思っていて Rust のマクロを調べていた事もあったんですが Rust のマクロがまさに AST レベルでコードを変更するような機能になりますね。
とはいえ構想だけで1年ぐらい何もしていなかったんですが今回の RubyKaigi をきっかけに実際に実装してみた、って感じですね。

RubyKaigi への登壇のきっかけ

最初は RubyKaigi に登壇する事は考えてなかったんですが知り合いが CFP を出すという事で『じゃあ、わたしも出してみるか〜〜〜』というのが直接的なきっかけになります。
Ruby のマクロの話も前々からどこかではしたいと思っていたのでそれで CFP を出してみたって感じです。
なので CFP を書き始めたのは結構ギリギリでしたね。
具体的には CFP の締切が6月末で、6月23日から CFP を書きはじめました。
時系列的には以下のような感じですね。

全体的な時系列

  • 2020/07〜08頃:この頃から AST -> Ruby に変換する実装のたたき台を書き始める
  • 2020/09〜:各 AST の種類から Ruby のコードに変換する処理をチマチマ書き始める
    • 1日1対応する、みたいなことをしていた記憶
  • 2020/12/01:Ruby Advent Calendar で AST -> Ruby のコードに変換する記事を書く
  • 2021/06/23:知り合いに触発されて RubyKaigi の CFP を書き始める
  • 2021/06/30:RubyKaigi の CFP 締切
    • 知り合いに翻訳のチェックなど行ってもらいだいぶ助かった…
  • 2021/07/01:CFP の結果はまだだったがすぐに作業を開始
    • Scrapbox を用意
    • Scrapbox に TODO リストや作業ログ、たたき台などを残していった
    • 最初は rensei-gem の残り作業を拾うところから
      • 無限に Rensei のバグ修正をしていた
  • 2021/07/05:Scrapbox に日報を書くように開始
    • 今回これをやったのがかなりよかった
    • ほぼほぼ毎日書いてた
  • 2021/07/14:CFP が Accept されたと連絡をもらう
    • 録画かリアルタイムか選択できて無限に悩み始める
      • 内容が膨れることはわかっていたので録画の方がよさそうだなあ、と思いつつリアルタイムの方がギリギリまでスライドをかけるので…
      • 結局リアルタイムを選択
  • 2021/07/14:ここから『Ruby のマクロとは…』を考え始める
    • Rensei のバグ修正に終わりが見えずにこのままではやばいと気づき始める
      • Rensei のバグ修正は最悪直ってなくても大丈夫ではあった
    • この時にはじめて kenma-gem(Ruby のマクロを実装する gem)のたたき台を書き始める
  • 2021/07/23:スライドのアウトラインを考え始める
  • 2021/07/28:kenma-gem の仮実装が固まる
    • 一段落したのでここからしばらく虚無が続く…
    • 仮実装を知り合いに見てもらいつつ意見をもらい始める
  • 2021/08/01:作業再開
    • kenma(仮実装)を gem 化した
  • 2021/08/11:実際に kenma-gem でオレオレマクロを定義しつつ課題を探し始める
  • 2021/08/23:スライドを書き始め、ここで Scrapbox のログが途絶える
    • ここからしばらく(プライベートで)超絶つらい期間が続く…
    • 何も記憶がない
    • ひたすらスライドを書いては知り合いに壁打ちをお願いしてたような…
  • 2021/09/11:登壇当日
    • なのか無事に終わらせた…

スライドに関して

スライド作成はいつも Reveal.js を使っていたんですがもうちょっとリッチなスライドをつくりたくて今回 slidev を使用してスライドをつくりました。
最初は slidev でいい感じに作れるやろ〜〜〜と軽く考えてたんですが実際はスライドを作っていた期間のうち半分は slidev と戦っていましたね…。なかなかいい感じにアニメーションとか作ることができなくて…。
スライド自体もなるべく前提知識がなくてもわかるように構成しようとすると無限にスライドが膨らんでしまいどこを削るのかかなり悩んでいました。
RubyVM::AST の説明は最後まで入れるべきかどうか悩んでいましたねえ…。今思うと入れなくてもよかったかなあ…。
本当は rensei-gem や kenma-gem の実装の話とかもしたかったんですが今回はあくあまでも『Ruby のマクロとは』という店にフォーカスを当てた内容にしました。
実装に関してはまたどこかで機会があれば話してみたいですねえ。

所感

はい、ということで RubyKaigi 初参加で初登壇する実績を解除しました。
今回はじめての参加だったので当日まで本当にどうなってしまうのかめちゃくちゃ不安だったんですがなんとか(登壇も時間どおり)無事に終わらせることができたのでそこが一番よかったです。
登壇自体は事前にかなり練習していたのでそれはやってよかったです。
内容的には結構ウケてたんですかね?あんまり実感がなくてよくわからないんですが終わった後に特に話を聞くこともないのでまあそんな感じだったのでしょう。
そういう意味では手応え的なのは全然なかったですねえ。
それはそれとして個人的には言いたかった事は全部言えたと思うので満足感はかなりあります。
マクロの機能自体も当初から想像していた形にほぼほぼできたと思うのでそういう意味だと達成感がありすぎて逆にやる気が虚無になっていますね…。
登壇でも言ってたんですが、ファイル単位でガッツリ書き換える、っていう運用はまだまだ先だろうけど局所的にマクロを使うのはそんなにハードルが高くないとは思っているのでそのあたりは実用的にしてみたいなあ、という気持ち。
あとマジで Scrapbox にログを残すようにする運用はよかったので今後も利用していきたい。
最後に運営の方々、マクロ実装・登壇の壁打ちに付き合ってくれた方々ありがとうございましたー。

反省

  • スライドが大盛りになって時間がかなりカツカツになってしまった…
    • かなり削ったけどもうちょい削れる事ができそうだったなあ…
    • 結果的には時間ちょうどで終わらせる事ができたけどゆっくり喋れなくて翻訳の方に申し訳ない…
  • Rensei が対応している Ruby のバージョンを表記しておくべきだった
    • Ruby 2.6 ~ 3.1dev まで対応している
  • スライドの作成がギリギリになっていた
    • 練習するたびにスライドのミスを見つけてつらい
    • 最後の1週間はずーっとスライドの練習に付き合ってもらっていた。めっちゃ感謝
  • 実際の配信画面を見てなかったので実際にどう映っていたのかなんもわからねえ

RubyKaigi で気になった発表

  • TypeProf for IDE: Enrich Dev-Experience without Annotations
    • Ruby の TypeProf を利用して静的片付けのような開発体験を実現する話
    • 静的片付けがほしいのではなくて開発体験がほしいわかる
      • 静的片付けがほしいのではなくて開発体験をよくしたい〜〜〜
    • シグネチャ表示がほしい…ほしい…
    • VSCode 高機能なので Vim でもほしい
    • 静的コードチェックは Rubocop 使っているけどもうちょい別の Lint を使いたい
      • 言語的な意味でのエラーチェックを行ってほしい
    • gem のコード開発は基本的に自身では呼ばれない事があるのでそのあたりどう TypeProf で解析するようになるのかは気になる
      • テストコードを含めて TypeProf で解析するようにしたい
    • RBS ファイルをシュッと記述する機能は Vim にもほしい
      • こういうのは TypeProf 側で提供されている機能になるんかな
      • なるべく VSCode に依存しないような機能として提供されてほしい
    • プロジェクトごとに TypeProf の LSP を起動するのはどうするのがいいんじゃろうか
  • The Art of Execution Control for Ruby's Debugger
    • Ruby のデバッガの debug-gem の話
    • コンソール画面で入力コマンドが見えなくて sob
    • VSCode で変数の中身が見れるの便利だなあ
      • Vim でもやりたい
    • break do: 便利そう
    • 今すぐ欲しいリモートデバッグ
    • 高機能なので使いこなせるかが不安
      • 使いこなしたい
    • step back すごい
    • デバッグだけ VSCode を使うのでもいい気がしてきた
    • TracePoint を使って実装されている、と
    • エディタとの連携部分がもうちょい知りたかった
  • Parallel testing with Ractors: putting CPUs to work
    • Ractor を使った並列テストを行うためのテストフレームワークの話
    • どう並列テストを行うかの解説がわかりやすい
    • Ractor をある程度理解してないとコードをその場で追うのは大変かも
    • テストフレームワークの実装や処理の流れが理解しやすかった
    • デモのテスト実行中にいろいろと操作できるのがすごい
    • Ractor の制限とかバグ無限につらそう
  • Demystifying DSLs for better analysis and understanding
    • RubyDSL を静的解析の話
    • Ruby のコードを例にして DLS を解説しているので『DSL is なに』みたいな人が見てみると理解が捗りそう
      • メタプロの話もでてくるのでそのあたりも学習につながる
    • DSL から RBI を生成し、 RBI で静的チェックなどを行う
    • RubyVim の親和性をもっとよくしたい
  • Parsing Ruby
    • Ruby の歴史を追っていく話
    • 知らない話がでてきて面白い
    • 今までいろんなことをやってきたんだなあ
      • parser-gem は 2.0 の時代につくられたのか
  • include/prepend in refinements should be prohibited
    • Refinements つらい話
    • 話の中であったハマりポイントは実際自分も何度かハマっているので代替機能はほしいんだが〜〜〜
    • include / prepend は多用しているので将来的に deprecated になるのは怖いがある程度しょうがなさそう
    • 代替機能があるならまあがんばって移行していくしかないなあ
  • Beware the Dead End!!
    • end がなかった時などのシンタックスエラーがどこでエラーになっているのかを検知する gem の話
    • 内容がコミカルで面白い
    • gem-dead_end 今すぐ使いたい!!
  • Graphical Terminal User Interface of Ruby 3.1
    • 今年も沢登りの話、と思いきや reline にダイアログ機能が実装されて irb で自動補完が追加された話
    • 風景はあいかわらずだけど内容はガチ
    • 実際に最新の irb を使うとかなりインパクトがでかい
  • Dive into Encoding

2021/10/14 今回の気になった bugs.ruby のチケット

今週は feature チケットに関するトリアージガイドラインの提案などがありました。

[Bug #18250] Anonymous variables seem to break Ractor.make_shareable

  • 以下のようなコードを実行すると Ractor.make_shareableTypeError が発生するというバグ報告
def foo(*); ->{ super };end
# error: `make_shareable': wrong argument type false (expected Symbol) (TypeError)
Ractor.make_shareable(foo) # expected Symbol
  • TypeError ではなくて Ractor::IsolationError が発生するのが期待する挙動らしい
  • ちなみに Ractor.make_shareable は Ractor 間でオブジェクトを使用するために引数を不変にするメソッド
  • #freeze とは違ってネストしてオブジェクトを freeze する
# freeze は配列の中身までは freeze しない
ary = [{ a: 1, }, { b: 2 }]
ary.freeze
pp ary.frozen?      # => true
pp ary[0].frozen?   # => false
pp ary[1].frozen?   # => false

# freeze は配列の中身までは freeze しない
ary = [{ a: 1, }, { b: 2 }]
Ractor.make_shareable(ary)
pp ary.frozen?      # => true
pp ary[0].frozen?   # => true
pp ary[1].frozen?   # => true

[Bug #18246] send does not work for unary ! operator when operator isn't a literal symbol

  • ! 単項演算子send で呼び出すとエラーになるというバグ報告
# これは + 二項演算子を呼ぶ
1.send(:+, 2)    # => 3
1.send(:"+", 2)  # => 3

# これは - 単項演算子を呼ぶ
1.send(:-@)   #=> -1
1.send(:"-@") #=> -1

# これは ! 単項演算子を呼び出してほしい
false.send(:!@)   #=> true
# error: undefined method `!@' for false:FalseClass (NoMethodError)
false.send(:"!@")
  • これは期待する挙動で ! 単項演算子を呼び出す場合は ! メソッド名で呼び出す必要がある
    • false.send(:"!") # => true
  • 逆に false.send(:!@) がなぜ動くのかと言うと :!@:! になるので false.send(:!) と同じ意味になるから
p :!@     # => :!
p :"!@"   # => :"!@"
  • 知らんかった

[Misc #18248] Add Feature Triaging Guide

Rubyは2019年6月にバグトリアージガイドを追加しましたが、それ以降はそれを使ってトラッカーのオープンバグを1400以上から300程度まで減らしました。Rubyでは現在、課題トラッカーに1200以上のオープンな機能リクエストがあります。ざっと見たところ、これらの多くはすでに実装されており、多くは望まれていないと思われます。私はRubyに機能トリアージガイドを追加し、機能リクエストのトリアージを開始したいと考えています。オープンな機能リクエストは、まだ実装されていない機能のうち、Rubyコアチームが実装を希望したり、パッチを検討したりするものを目標としています。そうすることで、潜在的Rubyへの貢献者が、自分ができる貢献の可能性を簡単に知ることができるようになります。

Ruby の debug-gem をつかってみた

RubyKaigi で発表があった debug-gem を使ってみたので覚書。
思ったよりもいろんな機能があった。

インストール

$ gem install debug

でインストールするか Gemfile に以下を追加して debug-gem を導入します。

gem "debug", ">= 1.0.0"

最新版を使いたい場合はこう。

gem "debug", git: "git@github.com:ruby/debug.git", branch: "master"

簡単な使い方

いくつか使い方はあるが今回は binding.break を使ってデバッグしてみる。

コード

  • 処理を止めたい場所に binding.break を仕込んでおく
    • ここで処理が止まる
require 'debug'

a = 1
b = 2
# binding.irb のように binding.break で処理が止まる
binding.break
c = 3
d = 4
binding.break
p [a, b, c, d]

デバッグ実行

  • 通常通り ruby コマンドでコードを実行すれば OK
  • binding.break したところで処理が止まる
$ bundle exec ruby sample.rb
[1, 9] in sample.rb
     1| require 'debug'
     2|
     3| a = 1
     4| b = 2
= >   5| binding.break
     6| c = 3
     7| d = 4
     8| binding.break
     9| p [a, b, c, d]
= >#0   <main> at sample.rb:5
  • ここで info locals を入力して確定するとローカル変数一覧が表示されたりする
$ bundle exec ruby sample.rb 
[1, 9] in sample.rb
     1| require 'debug'
     2| 
     3| a = 1
     4| b = 2
= >   5| binding.break
     6| c = 3
     7| d = 4
     8| binding.break
     9| p [a, b, c, d]
= >#0   <main> at sample.rb:5
(rdbg) info locals    # command
%self = main
a = 1
b = 2
c = nil
d = nil
(rdbg) 
  • 他にも nextstep でステップ実行したり p {Ruby の式} で式の実行結果を表示したりいろいろな機能がある
  • 詳しくは README を参照

所感

  • binding.irb みたいに binding.break で処理を止めてデバッグするのでとっつきやすいっちゃとっつきやすい
  • 対話時にコマンドを入力すると右側にコメントで注釈が出てくるのが便利
  • debug-gem 便利そう!と思いつつ byebug も使いこなせてないので debug-gem 使いこなせるかどうか…
  • プロセスへのアタッチはまだ試してないので試しておきたい
    • 既存の Rails へのプロセスへアタッチして中身をデバッグしてみたりとかしたいなあ…
  • RubyKaigi の登壇だとあんまり VSCode との連携の話が出てなかったのでこのあたりが気になる
    • DAP をサポートしているみたいなので VimDAP プラグインといい感じに接続できないかなあ…
  • Ruby 3.1 がいろいろと楽しみになってきた
  • 次は step back の記事を書くぞ…

2021/10/02 今回の気になった bugs.ruby のチケット

今週は YJIT の導入チケットがつくられました。

[Feature #18229] Proposal to merge YJIT

[Feature #18159] Integrate functionality of dead_end gem into Ruby

2021/09/23 今回の気になった bugs.ruby のチケット

今週は min / max / minmax で比較する値とその結果の要素を返すメソッドの提案などがありました。

[Bug #18187] Float#clamp() returns ArgumentError (comparison of Float with 1 failed)

  • Float::NAN.clamp(0, 100) すると ArgumentError が発生するがこれは期待する挙動ではない、というチケット
  • 例えば以下のように Ruby#clamp を実装した場合は Float::NAN を返す
Float.define_method(:clamp2) { |min, max| self < min ? min : self > max ? max : self }

p 8.0.clamp2(10, 100)
# => 10
p 80.0.clamp2(10, 100)
# => 80.0
p 800.0.clamp2(10, 100)
# => 100
p Float::NAN.clamp2(10, 100)
# => NaN

[Bug #18188] -1 ** 0 is 1 not -1

  • -1 ** 01 を期待するが実際には -1 が返ってくる、というバグ報告
  • が、Ruby-1 ** 0-(1 ** 0) としてパースするのでこれは期待する挙動になっている
    • (-1) ** 01 が返ってくる
# これは -(1 ** 0)
p(-1 ** 0)
# => -1

p(-(1 ** 0))
# => -1

p((-1) ** 0)
# => 1

[Feature #18181] Introduce Enumerable#min_with_value, max_with_value, and minmax_with_value

  • Enumerable#min_with_value, max_with_value, and minmax_with_value を追加する提案
  • これは比較するための値とその要素を一緒に返すためのメソッドになる
# 要素と比較する size の値の両方を結果として返す
%w(abcde fg hijk).min_with_value { |e| e.size } # => ['fg', 2]
%w(abcde fg hijk).max_with_value { |e| e.size } # => ['abcde', 5]
%w(abcde fg hijk).minmax_with_value { |e| e.size } # => [['fg', 2], ['abcde', 5]]
  • 引数を渡すとその個数分の結果も返ってくる
%w(abcde fg hijk).min_with_value(2) { |e| e.size } # => [['fg', 2], ['hijk', 4]]
%w(abcde fg hijk).max_with_value(2) { |e| e.size } # => [['abcde', 5], ['hijk', 4]]
  • 現状だと以下のように書く必要がある
    • のでもっと簡素に書きたいというのが要求みたい?
p %w(abcde fg hijk).map { |e| [e.size, e] }.min_by(&:first)
# => [2, "fg"]
%w(abcde fg hi jkl mn).min_by_with_elements(&:size) # => [2, ["fg", "hi", "mn"]]
  • ただし #min_by は複数の要素があっても単一の要素を返すのでそっちに合わせるなら1つがよさそう?
p %w(abcde fg hi jkl mn).min_by(&:size)
# => "fg"

[Feature #18179] Add Math methods to Numeric

  • Math.sqrt などのクラスメソッドの x.sqrt のように Numericインスタンスメソッドにするチケット
  • オブジェクト指向的にはインスタンスメソッドの方が自然な気がする
  • このチケットとは関係ないんですがこういう Hoge.foo(x) みたいなメソッドをシュッと x.foo みたいに呼び出せる機能がほしい
    • パイプライン演算子みたいなのがほしいって言うわけではなくても Hoge モジュールをインスタンスメソッドとして組み込むような機能

[Bug #18170] Exception#inspect should not include newlines

  • 次のようにエラー内容に改行が含まれている場合、標準出力でも改行されてしまう
p StandardError.new("foo\nbar")
#=>
# #<StandardError: foo
# bar>
  • これを #<StandardError: "foo\nbar"> を返すようにするのはどうか、という提案
  • did_you_meanerror_highlight で複数行のエラーを表示する際に出力が奇妙になるらしい
class Foo
  def initialize
    @exception = begin; exampl; rescue Exception; $!; end
  end

  def example
  end
end

p Foo.new
#=>
# #<Foo:0x00007f15aeb4ba48 @exception=#<NameError: undefined local variable or method `exampl' for #<Foo:0x00007f15aeb4ba48 ...>
#
#     @exception = begin; exampl; rescue Exception; $!; end
#                         ^^^^^^
# Did you mean?  example>>

[Bug #17048] Calling initialize_copy on live modules leads to crashes

  • 以下のコードで Ruby がクラッシュするというバグ報告
loop do
  m = Module.new do
    prepend Module.new
    def hello
    end
  end

  klass = Class.new { include m }
  m.send(:initialize_copy, Module.new)
  GC.start

  klass.new.hello rescue nil
end
  • Module#initialize_copy を呼び出すとクラッシュする可能性があるらしい
  • initialize_copy はレシーバを引数のオブジェクトの内容で置き換えるメソッド
  • 最新版では Module#initialize_copy を呼び出すと TypeError が発生するように修正されている
module A
end

# error: `initialize_copy': already initialized module (TypeError)
A.send(:initialize_copy, Module.new) # fine, no one inherits from A

2021/09/16 今回の気になった bugs.ruby のチケット

今週は { a: a, b: b }{ a:, b: } とかけるショートハンドが入りました。

[Feature #18168] Add ActiveSupport deep_transform_values to Ruby

require "active_support/all"

hash = { person: { name: 'Rob', age: '28' } }

# ネストした Hash すべてに value.to_s.upcase を適用させる
pp hash.deep_transform_values { |value| value.to_s.upcase }
# => {:person=>{:name=>"ROB", :age=>"28"}}
  • 再帰的に参照している場合は SystemStackError になっちゃうみたいですね
require "active_support/all"

hash = { a: 1 }
hash[:hash] = hash

# 再帰的に参照してる場合はエラー
# error: stack level too deep (SystemStackError)
pp hash.deep_transform_values{ |value| value.to_s.upcase }
  • チケットの内容が虚無なのでこのままだと流れていきそうな雰囲気

[Bug #18160] IndexError raised from MatchData#{offset,begin,end} does not keep the encoding of the argument

pp RUBY_VERSION  # => "3.0.2"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: \xE3\x81\x82"
  • これは最新版では修正されて正しい意図する文字コードで出力される
pp RUBY_VERSION  # => "3.1.0"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: あ"
  • 便利

[Feature #14579] Hash value omission

  • { x:, y: }{ x: x, y: y } のショートハンドにする提案
  • これ以外にも似たような提案は無限にされていたけど matz を説得できずに長年入っていなかった
  • 先日の RubyKaigi の感想戦でこれに関する議論がされて、matz を説得して無事に accepts された :tada:
  • これは以下のように x:x: x に展開するようなシンタックスシュガーになる
name = "homu"
age = 14

# { name: name, age: age } のシンタックスシュガー
{ name:, age: }
# => {:name=>"homu", :age=>14}

# 一部だけ値を割り当てることもできる
{ name:, age: 16 }
# => {:name=>"homu", :age=>16}


def tokyo
  "東京"
end

# メソッド呼び出しも可能
{ tokyo: }
# => {:tokyo=>"東京"}


def user(name:, age:)
  { name:, age: }
end

# user(name: name, age: age) になる
user(name:, age:)


# 現時点では定数も展開できる
X = 42
{ X: }
# => {:X=>42}
  • 現状は { X: } みたいな定数も展開されるがもしかしたらリリース時点でまた仕様が変わっている可能性があるので注意
  • ちなみにこれを利用すると if 変数に対して高速にアクセスする事ができる
def foo(if:)
  # if はキーワードなので変数にアクセスする時は Binding#local_variable_get を使う必要があった
  if_ = binding.local_variable_get(:if)

  # このショートハンドを使うとこうかけるようになる
  if_ = {if:}[:if]
end

[Feature #17355] Using same set of names in or-patterns (pattern matching with Foo(x) | Bar(x))

  • 以下のようにパターンマッチで束縛名が重複していると今はエラーになる
case [1, 2]
in [1, a] | [a, 3] then a
end
  • これをエラーではなくて各パターンごとで束縛できるようにする提案
  • これがあると例えば以下のようなコードが
def user_email(user)
  case user
  in User(email:) then email
  in Admin(email:) then email
  in Moderator(email:) then email
  end
end
  • 以下のように1つのパターンで定義する事ができる
def user_email(user)
  case user
  in User(email:) | Admin(email:) | Moderator(email:) then email
  end
end
  • 今はまだ実装中?らしい
  • これは普通にほしいなあ

[Misc #18150] Proposal: Deprecate leading zero syntax to declare octals, since it's extremely confusing (and Python 3 removed it too)

  • 8進数リテラルについて議論するチケット
  • 現状の Ruby0 が先頭に付いていると8進数として解釈される
    • 012 # => 10
  • また 0o が付いている場合も8進数として解釈される
    • 0o12 # => 10
  • なのでうっかり次のようなコードを書いてしまうとこれはエラーになってしまう
# "2021-09-01" のつもりで 09 と書いてしまった
# これはエラーになる
START_DATE = Date.new(2021, 09, 01)
# error: Invalid octal digit
  • このようなケースがあるので 0 を使った8進数リテラルRuby 3.x では非推奨にし、Ruby 4.0 では 0011 => # 11 にしよう、という提案
  • ちなみに先頭 0 が8進数リテラルになっている言語は JavaScript Go Java C がある
  • 逆にそうでない言語は RustElixir とのこと
  • また Python3+ はエラーになる
  • 個人的には 8進数リテラルを使ったことがないので気にはならないけど、既存のコードを非互換にしてまで入れる必要があるのかと言われると難しいところ

[Feature #18146] Add delete_prefix and delete_suffix to Pathname

  • Pathname#delete_prefixdelete_suffix を追加する提案
  • 元々は RuboCop が誤検知していたのが起因らしい
  • 誤検知を本体に実装しようとするアイディアがすごい(褒めてる)

[ruby/un] Add colorize command

  • 普通に便利そう

[Feature #16182] Should expr in a, b, c be allowed or not?

  • 元々は Ruby 2.7 時点での expr in [a, b, c] という1行 in の [] を省略できるようにするかどうかを議論するチケット
  • Ruby 2.7 (3.0) では []{} は省略できないようになっていた
# これらはシンタックスエラー
{ name: "homu" } in name:
[1, 2] in a, b
# OK
{ name: "homu" } in name:
[1, 2] in a, b
  • ちなみに右代入も許容されている
# OK
{ name: "homu" } => name:
[1, 2] => a, b
  • また、メソッドの引数に渡す場合は明示的に () を付ける必要があるので注意する
# これはエラー
puts([1] in String)

# () をつけると OK
puts(([1] in String))

Ruby 3.1 で { a: a } が { a: } とかけるようになるらしい

以前から山のように提案があった Hash のショートハンドが RubyKaigi の感想戦で matz を説得して入ったらしい。

これによって Ruby 3.1 で Hash が以下のように書くことができます。

name = "homu"
age = 14

# { name: name, age: age } のシンタックスシュガー
{ name:, age: }
# => {:name=>"homu", :age=>14}

# 一部だけ値を割り当てることもできる
{ name:, age: 16 }
# => {:name=>"homu", :age=>16}


def tokyo
  "東京"
end

# メソッド呼び出しも可能
{ tokyo: }
# => {:tokyo=>"東京"}


def user(name:, age:)
  { name:, age: }
end

# user(name: name, age: age) になる
user(name:, age:)

めっちゃ便利ですね!!!!
このショートハンドは { a: } -> { a: a } というよりかは a: -> a: a になる、と認識するとよさそうです(なので [a:] -> [a: a] となる。
matz を説得している時にちょうど離席していたのでどのような経緯で matz を説得したのかがすごく気になる…。
これは既に本体にコミットされているので rbenv install 3.1.0-dev などで最新の Ruby を落としてくるとすぐに使えます。
今すぐ早く使いたい。

おまけ

感想戦binding.local_variable_get(:if) の変わりに {if:}[:if] が使えるようになると言及されててワロタ

def foo(if:)
  # if はキーワードなので変数にアクセスする時は Binding#local_variable_get を使う必要があった
  if_ = binding.local_variable_get(:if)

  # このショートハンドを使うとこうかけるようになる
  if_ = {if:}[:if]
end

便利そう。