初参加&初登壇の RubyKaigi Takeout 2021 で Ruby のマクロの話をしてきたよレポート
書こう書こうと思って気がついたら1ヶ月立っていましたこんにちは。
(去年を除いて)今回が RubyKaigi 初参加&初登壇ということでいろいろと書き残しておこうと思います。
ちなみに今週末に以下のようなイベントで雑に話すので興味がある方がぜひぜひ参加してみてくださいー。
ちなみに『Ruby のマクロ』という主語がクソデカなんですがあくまでも『この登壇では』という前提になります。
Use Macro all the time ~ マクロを使いまくろ ~
動画
補足
RubyVM::AST
はバージョンごとに非互換- 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 のコードに変換する記事を書く
- 【Ruby Advent Calendar 2020】Ruby の AST から Ruby のソースコードを復元しよう【1日目】 - Secret Garden(Instrumental)
- 同時に rensei-gem (AST -> Ruby のコードに変換する gem)も公開
- ここで一旦燃え尽きた
- 2021/06/23:知り合いに触発されて RubyKaigi の CFP を書き始める
- 2021/06/30:RubyKaigi の CFP 締切
- 知り合いに翻訳のチェックなど行ってもらいだいぶ助かった…
- 2021/07/01:CFP の結果はまだだったがすぐに作業を開始
- 2021/07/05:Scrapbox に日報を書くように開始
- 今回これをやったのがかなりよかった
- ほぼほぼ毎日書いてた
- 2021/07/14:CFP が Accept されたと連絡をもらう
- 録画かリアルタイムか選択できて無限に悩み始める
- 内容が膨れることはわかっていたので録画の方がよさそうだなあ、と思いつつリアルタイムの方がギリギリまでスライドをかけるので…
- 結局リアルタイムを選択
- 録画かリアルタイムか選択できて無限に悩み始める
- 2021/07/14:ここから『Ruby のマクロとは…』を考え始める
- Rensei のバグ修正に終わりが見えずにこのままではやばいと気づき始める
- Rensei のバグ修正は最悪直ってなくても大丈夫ではあった
- この時にはじめて kenma-gem(Ruby のマクロを実装する gem)のたたき台を書き始める
- Rensei のバグ修正に終わりが見えずにこのままではやばいと気づき始める
- 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
- Parallel testing with Ractors: putting CPUs to work
- Demystifying DSLs for better analysis and understanding
- 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
- Dive into Encoding
2021/10/14 今回の気になった bugs.ruby のチケット
今週は feature チケットに関するトリアージのガイドラインの提案などがありました。
[Bug #18250] Anonymous variables seem to break Ractor.make_shareable
- 以下のようなコードを実行すると
Ractor.make_shareable
でTypeError
が発生するというバグ報告
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
- feature チケットに対するトリアージのガイドラインを追加する提案
- チケットの優先順位やどういう時に Close するのかの決めごとなどのガイドライン
- チケットの内容を機械翻訳すると以下のような感じ
Rubyは2019年6月にバグトリアージガイドを追加しましたが、それ以降はそれを使ってトラッカーのオープンバグを1400以上から300程度まで減らしました。Rubyでは現在、課題トラッカーに1200以上のオープンな機能リクエストがあります。ざっと見たところ、これらの多くはすでに実装されており、多くは望まれていないと思われます。私はRubyに機能トリアージガイドを追加し、機能リクエストのトリアージを開始したいと考えています。オープンな機能リクエストは、まだ実装されていない機能のうち、Rubyコアチームが実装を希望したり、パッチを検討したりするものを目標としています。そうすることで、潜在的なRubyへの貢献者が、自分ができる貢献の可能性を簡単に知ることができるようになります。
- 関連として 2019年に bug チケットのトリアージのガイドラインを追加している
- https://bugs.ruby-lang.org/issues/15943
- 結果的に 1400以上あった bug チケットが300程度まで減ったらしい、すごすぎる
- ガイドライン自体は PR にかかれているので気になる人は見てみるとよいかも
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)
- 他にも
next
やstep
でステップ実行したりp {Ruby の式}
で式の実行結果を表示したりいろいろな機能がある - 詳しくは README を参照
所感
2021/10/02 今回の気になった bugs.ruby のチケット
今週は YJIT の導入チケットがつくられました。
[Feature #18229] Proposal to merge YJIT
- Ruby 本体に YJIT を組み込む提案
- matz が +1 している での本体には取り込まれそう
- 来月リファクタリングされるらしいので実際に入るのはもうちょい後かも?
- Ruby 3.1 に間に合うといいんだが〜〜〜
- RubyKaigi の YJIT のアーカイブ見ておかんと…
[Feature #18159] Integrate functionality of dead_end gem into Ruby
- RubyKaigi で話にでてた
dead_end
gem を本体に取り込む提案 - 取り込む事自体は概ね問題ないっぽいがいくつか懸念点があるみたいなので Ruby 3.1 には間に合わなさそうな雰囲気
- https://bugs.ruby-lang.org/issues/18159#note-7
- 3.1 のリリースまであと3ヶ月しかなくてもうちょっと長期的に検証を行いたいみたい
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
- なので組み込みの
#clamp
もNaN
を返すのが期待する挙動では?という旨らしい
[Bug #18188] -1 ** 0 is 1 not -1
-1 ** 0
は1
を期待するが実際には-1
が返ってくる、というバグ報告- が、Ruby は
-1 ** 0
を-(1 ** 0)
としてパースするのでこれは期待する挙動になっている(-1) ** 0
は1
が返ってくる
# これは -(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"]
- 具体的なユースケースがパッと思い浮かばなかった
- チケットには巡回セールスマン問題を特例が載っている
- https://bugs.ruby-lang.org/issues/18181#Example
- コメントで『最小(最大)の要素が複数ある場合に複数の要素を返す必要がある』などと指摘されてる
- その上で以下のように複数の要素を返すようにするといいのでは?とコメントされてる
%w(abcde fg hi jkl mn).min_by_with_elements(&:size) # => [2, ["fg", "hi", "mn"]]
- ただし
#min_by
は複数の要素があっても単一の要素を返すのでそっちに合わせるなら1つがよさそう?- 複数の要素を返す場合に大量にメモリを消費し、遅くなってしまうとコメントもされている
- https://bugs.ruby-lang.org/issues/18181#note-2
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
みたいに呼び出せる機能がほしい
[Bug #18170] Exception#inspect should not include newlines
- 次のようにエラー内容に改行が含まれている場合、標準出力でも改行されてしまう
p StandardError.new("foo\nbar") #=> # #<StandardError: foo # bar>
- これを
#<StandardError: "foo\nbar">
を返すようにするのはどうか、という提案 did_you_mean
やerror_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
はレシーバを引数のオブジェクトの内容で置き換えるメソッド- インスタンス変数などで保持していない内部情報をコピーするためのメソッド
- https://docs.ruby-lang.org/ja/latest/method/Object/i/initialize_copy.html
- 最新版では
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
- ActiveSupport の
#deep_transform_values
を 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
MatchData#{offset,begin,end}
で発生したIndexError
がエンコーディングを保持してないバグ報告
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 }
のショートハンドにする提案- JavaScript にあるような奴
- これ以外にも似たような提案は無限にされていたけど 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進数リテラルについて議論するチケット
- 現状の Ruby は
0
が先頭に付いていると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
がある - 逆にそうでない言語は
Rust
とElixir
とのこと - また
Python3+
はエラーになる - 個人的には 8進数リテラルを使ったことがないので気にはならないけど、既存のコードを非互換にしてまで入れる必要があるのかと言われると難しいところ
[Feature #18146] Add delete_prefix
and delete_suffix
to Pathname
Pathname
に#delete_prefix
とdelete_suffix
を追加する提案- 元々は RuboCop が誤検知していたのが起因らしい
- https://github.com/rubocop/rubocop-performance/issues/245
Pathname.new("path/to/some/ruby/file.rb").sub(/\.rb\z/, "")
- が
Pathname.new("path/to/some/ruby/file.rb").delete_suffix(".rb")
- を推奨するようになってしまっていたらしい
- 誤検知を本体に実装しようとするアイディアがすごい(褒めてる)
[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
- Ruby 3.1 からはこの条件が緩和されて
[]
や{}
が省略してかけるようになる
# 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 を説得して入ったらしい。
- チケット:[Feature #14579] Hash value omission
- コミット:https://github.com/ruby/ruby/commit/c60dbcd1c55cd77a24c41d5e1a9555622be8b2b8
- 感想戦のログ:https://hackmd.io/Mmse5ybASq-6c2kQoJQdMQ
これによって 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
便利そう。