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
便利そう。
2021/09/03 今週の気になった bugs.ruby のチケット
今週は Enumerable#take_while_after
や Enumerable#detect_only
といったメソッドの提案がありました
[Feature #18136] take_while_after
- 先頭からブロックが真になった要素までを返すメソッドの提案
tokens = [ {text: 'Ruby', type: :word}, {text: 'is', type: :word}, {text: 'cool', type: :word}, {text: '.', type: :punctuation, ends_sentence: true}, {text: 'Rust', type: :word}, # ... ] # _1[:ends_sentence] が真になるまでの要素を返す sentence = tokens.take_while_after { _1[:ends_sentence] } pp sentence # => [{:text=>"Ruby", :type=>:word}, # {:text=>"is", :type=>:word}, # {:text=>"cool", :type=>:word}, # {:text=>".", :type=>:punctuation, :ends_sentence=>true}] # これは slice_after.first と同じ挙動 sentence = tokens.slice_after { _1[:ends_sentence] }.first pp sentence # => [{:text=>"Ruby", :type=>:word}, # {:text=>"is", :type=>:word}, # {:text=>"cool", :type=>:word}, # {:text=>".", :type=>:punctuation, :ends_sentence=>true}]
- 元々は
#16441
で提案されていたんですが、こちらは使用用途が不明で Reject されており、今回はユースケースを添付して改めてチケットを立てた形っぽいですね - 今回のチケットもユースケースが適切かどうか、みたいな議論がされているぽい
- あと
take_while_after
よりもtake_upto
の方がいいのでは?みたいなコメントもされていますね - なーんかあったら便利そうなメソッドだと思うんですが具体的なユースケースが思い浮かばないなあ…
[Feature #18135] Introduce Enumerable#detect_only
#detect
で要素が見つからなかった場合に例外を返すEnumerable#detect_only
メソッドの提案ぽい?#detect
で要素が1個だけ見つかった場合は成功し、それ以外は例外を返すEnumerable#detect_only
メソッドの提案- 次のようなコードを
matches = array.select { |elem| some_method(elem) } raise if matches.size != 1 match = matches.first
- 以下のようにかけるようにする提案らしい
match = array.detect_only { |elem| some_method(elem) }
- これは以下のチケットと同じかな
[Bug #18126] Process termination three seconds after thread termination dumps core
- 次のようなコードを実行するとランダムで segv することがあるらしい
3000.times{Thread.new{}} sleep 2.99
$ ruby test.rb [BUG] Segmentation fault at 0x0000000000000440 ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-linux] $
- これは
- らしい
[Misc #18125] A strange behavior when same name variable/method coexist issue.
- 次のように同名のメソッドと変数を定義した時に奇妙な動作になるけどこれは期待する動作?という質問
def deploy_to "deploy_to" end deploy_to = "#{deploy_to} new place" # ^ これがメソッドではなくて変数を参照している p defined? deploy_to # => local_varible p deploy_to # => " new place"
- これは期待する動作でこの場合は変数が定義され値が割り当てられる前のローカル変数
deploy_to
を参照する - メソッドを参照したい場合は
()
を付けてメソッド呼び出しだと明示化する必要がある
def deploy_to "deploy_to" end deploy_to = "#{deploy_to()} new place" # ^ これはメソッド呼び出しになる p deploy_to # => "deploy_to new place"
RubyKaigi Takeout 2021 で Ruby のマクロについて話します
先日 RubyKaigi Takeout 2021 のスピーカーとスケジュールが発表されました。
わたしも 3日目の朝一に Use Macro all the time ~ マクロを使いまくろ ~
というタイトルで登壇させていただきます。
内容はタイトルにも書かれている通りマクロの話をします。
マクロと言ってもいわゆる C言語のようなプリプロセスマクロではなくて『AST レベルで Ruby の構文を別の構文へと変換すること』を Ruby のマクロと定義し、Ruby でどのようにマクロを表現するのか、みたいな話をする予定です。
まあもっとさっくりう言うと『Ruby でマクロを実装してみたらこうなりました』みたいな感じですかねえ。
例えばマクロを使用することで、
hoge.foo.bar
のようなコードを
hoge&.foo&.bar
のようにぼっち演算子呼び出しとして変換する事ができたり、
CONST_HOGE = [1, 2, 3]
というような定数定義しているコードを
CONST_HOGE = [1, 2, 3].freeze
のように暗黙的に freeze
したり、
![a, b, c]
というようなコードを
{ a: a, b: b, c: c }
みたいな Hash
に展開するようなことを実現することができます。
Ruby のマクロは以下のような過程を経て Ruby のコードから別の Ruby のコードへと変換しています。
-
↓ AST に変換
変換前の AST データ
↓ AST の種類などを参照しながら特定の AST を別の AST に変換
変換後の AST データ
開発中のコードをちょっとだけお見せすると Ruby のマクロを使うことによって以下のようなことを実現することができます。
# ※開発中のコードなので当日までに変わるかもね! using Macro::Refine::Source # 特定の構文を変換するマクロを定義する module MyMacro using Macro::Macroable # cat! というメソッド呼び出しを任意のコードに変換する def cat!(num = ast { 1 }) ast { "にゃーん" * $num } end macro_function :cat! # 範囲リテラルを freeze するコードに変換する def freezing(node, parent) ast { $node.freeze } end macro_node :DOT2, :freezing macro_node :DOT3, :freezing # 代入式を freeze するコードに変換する def assign_freezing(node, name:, value:) ast { $name = $value.freeze } end macro_pattern pat { $name = $value }, :assign_freezing end # 変換したい Ruby のコードを Proc で定義 body = proc { use_macro! MyMacro puts cat! puts cat!(3) value = [2, 3, 5, 7, 11, 13, 17].grep((0..10)) HOGE = "にゃーん" + "にゃーん" } # マクロを適用させた AST に変換する result = Macro.compile_of(body) # 変換させた AST から Ruby のソースコードを取得する pp result.source # 以下のようなコードに変換される __END__ # cat!(n) が "にゃーん" * n に変換される puts "にゃーん" * 1 puts "にゃーん" * 3 # (0..10) が (0..10).freeze に変換される # value = hoge が value = hoge.freeze に変換される value = [2, 3, 5, 7, 11, 13, 17].grep((0..10).freeze).freeze HOGE = ("にゃーん" + "にゃーん").freeze
上記のような書き方で Ruby のコードを AST レベルで別のコードへと変換するような実装を書いています。
ちなみに実装はすべて Ruby で行っているので MRI 側で特に何か機能を追加したりとかはしていません。
この登壇でメタプロの向こう側ってやつを見せてやりますよ(誇張。
と、言うことでこういう話が好きな方は当日までお楽しみに!
ちなみに去年を除くとこれが RubyKaigi 初参加になります。
RubyKaigi 初参加で初登壇の実績を解除したぜ…。
2021/08/19 今週の気になった bugs.ruby のチケット
今週は右代入で特定のケースでシンタックスエラーになるというバグ報告がありました。
[Bug #18084] JSON.dump
can crash VM.
require 'json' x = {} # 自身に自身を割り当てる x[:x] = x # machine stack overflow in critical region (fatal) p JSON.dump(x)
require 'json' x = {} x[:x] = x # Ruby 2.6 の場合 # error: stack level too deep (SystemStackError) p JSON.dump(x)
- これが期待する動作なのかどうか、という旨がチケットに書かれていたが最新版では
SystemStackError
になるように修正された
[Bug #18080] Syntax error on one-line pattern matching
- 次のようにカッコで囲まれていないパラメータを持つメソッドの戻り値を右代入で使用するとシンタックスエラーになる、というバグ報告
# パラメータがなかったり、カッコが付いている場合は OK p do end => a p a #=> nil p(1) do end => a p a #=> 1 # カッコがないパラメータがある場合はシンタックスエラーになる p 1 do end => a #=> # syntax error, unexpected =>, expecting end-of-input # end => a # ^~ # これは1行 in でも同様にシンタックスエラーになる p 1 do end in a #=> # syntax error, unexpected `in', expecting end-of-input # end in a # ^~
- このエラーは意図的ではないが修正するのは難しいらしい
2021/08/13 今週の気になった bugs.ruby のチケット
今週は instance_exec
に Method#to_proc
を渡すと意図しない例外が返ってくるというバグ報告や Ruby 3.0.2 で Hash#transform_keys!
を呼び出すとメモリリークをするという報告がありました。
[Feature #18070] attr
should be removed
class User # getter のみ定義される attr :id # true を渡すと setter も定義される attr :name, true end
- 非推奨なのは第二引数の
true / false
を渡すことなのでattr
自体は特に問題なさそう?
[Feature #1806] instance_exec
is just ignored when the block is originally a method
Method#to_proc
の結果は元のコンテキスト情報を持っているので次のようなコードは失敗する
f = -> (x) { a + x } class A def a 1 end end # これは f 内の a は A#a を参照する A.new.instance_exec(1, &f) # => 2 class B def b(x) a + x end end # B#method の場合の場合は a が B#a を参照する proc = B.new.method(:b).to_proc # なのでエラーになる A.new.instance_exec(1, &proc) # => undefined local variable or method `a' for #<B:0x00007fdaf30480a0> (NameError)
- このチケットの提案自体は
NameError
になるのではなくてArgumentError
になるべきでは?という内容 - これ自体は期待する挙動らしい
- なぜなら
Method#to_proc
の実装は以下のようになっており結果的にMethod#call
を呼んでいるから
class Method def to_proc method = self ->(*args, **kwargs, &block) do # ここで `B#b` が呼ばれるため結果的に NameError になる method.call(*args, **kwargs, &block) end end end
- 今回の提案のように
Method#call
の挙動を変更すると挙動が壊れてしまうらしいModule.module_eval(&m)
みたいにmodule_eval
に渡したときの挙動が壊れてるぽい?- 実装: https://github.com/jeremyevans/ruby/commit/3e2db2f01281f2335c638142223f8b24531826bd
- コケてるテスト: https://github.com/jeremyevans/ruby/runs/3283493175
[Bug #18065] 3.0.2 - possible memory leak in Hash#transform_keys!
h = { value1: 1, value2: 2 } loop { h.transform_keys!(&:to_s) }
- この問題は既に最新版で修正済み
2021/08/05 今週の気になった bugs.ruby のチケット
今週はネストしたループで caller_locations
を呼び出すと segv するバグ報告などがありました。
[Feature #18057] Introduce Array#mean
- 配列の平均値を求める
Array#mean
メソッドを追加する提案- 元々は
Array#average
という名前だったが#mean
という名前に変わった
- 元々は
array = [1, 2, 3] array.mean # 2 # 要素の値を変更しながら適用 array = [1.5, 2.2, 3.1] array.mean(&:round) # 2.3333333333333335 array = [-3, -2, -1] array.mean { |e| e.abs } # 2
- これに関連していくつかの gem が紹介されている
- enumerable-statistics
Enumerable#mean
にも対応している
- Refinements
- enumerable-statistics
[Bug #18053] Crashes and infinite loops when generating partial backtraces in Ruby 3.0+
- 以下のコードを実行すると segv するというバグ報告
- Ruby 3.0 以降で再現する
def foo caller_locations(2, 1).inspect # this will segv # caller_locations(2, 1)[0].path # this will infinite loop end 1.times.map { 1.times.map { foo } }
- Ruby 3.0 の最適化のバグっぽい?
- 修正 PR は既に出てる
- https://github.com/ruby/ruby/pull/4671
- 修正後の方が2倍早くなっているらしい
- Ruby 3.0 に関しては最適化を元に戻すのが最善らしい
- 実装が複雑になっているので今回のバグを対応する場合は更に実装が複雑になるとのこと
- ただし、最適化を維持したい人がいれば別途対応するとのこと
- https://bugs.ruby-lang.org/issues/18053#note-1
[Bug #18052] Find のignore_error オプションが、文字化けファイル遭遇時の例外に対応していない (Windows)
Find.find(".", ignore_error: true)
のようにignore_error: true
を指定しても文字化けしたファイルがあると例外が発生するらしい
# coding:cp932 p __ENCODING__ # cp932 では表現できないファイル名を作る(例はハングル文字)。 open("testfile-\uD7A3 .jpg", "w") require "find" Find.find(".", ignore_error: true ) {|f| p f } # => #<Encoding:Windows-31J> # "." # "./testfile-???R?s?[.jpg" # Traceback (most recent call last): # 25: from d:/opt/ruby/bin/irb:23:in `<main>' # 24: from d:/opt/ruby/bin/irb:23:in `load' # 23: from d:/opt/ruby/lib/ruby/gems/2.6.0/gems/irb-1.3.2/exe/irb:11:in `<top (required)>' # 6: from (irb):10:in `<main>' # 5: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:43:in `find' # 4: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:43:in `each' # 3: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:48:in `block in find' # 2: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:48:in `catch' # 1: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:51:in `block (2 levels) in find' # d:/opt/ruby/lib/ruby/2.6.0/find.rb:51:in `lstat': Invalid argument @ rb_file_s_lstat - ./testfile-?[.jpg (Errno::EINVAL)
- これは既に別のチケットで修正済み
- https://bugs.ruby-lang.org/issues/14591
- ちょうど1ヶ月前に修正されていた