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ヶ月前に修正されていた
2021/07/29 今週の気になった bugs.ruby のチケット
Integer.try_convert
の追加や Struct.new.keyword_init?
の追加などがありました。
[Feature #10473] Change Date#to_datetime to use local time
require "date" # ローカルのタイムゾーンを使用しない p Date.new(2014,1,1).to_datetime.to_time # => 2014-01-01 00:00:00 +0000 # to_date や to_time はローカルのタイムゾーンを参照している p Date.new(2014,1,1).to_date.to_time # => 2014-01-01 00:00:00 +0900 p Date.new(2014,1,1).to_time # => 2014-01-01 00:00:00 +0900
p Time.parse('2021-07-23') # => 2021-07-23 00:00:00 +0900 p DateTime.parse('2021-07-23') # => #<DateTime: 2021-07-23T00:00:00+00:00 ((2459419j,0s,0n),+0s,2299161j)>
- また、
to_datetime
でローカルのタイムゾーンを使用するようにすると次の結果が変わってしまう
DateTime.parse(d.to_s) == d.to_datetime
DateTime
自体、互換性のために残されているので非互換にするのは望ましくない、との事- 互換性を保ちつつ対応する場合は
to_datetime
に使用するタイムゾーンを指定するようにするのはどうか、と提案されている
[Bug #18018] Float#floor / truncate sometimes result that is too small.
- 次のように
Float#floor
を使用すると意図しない結果になっているというバグ報告Float#floor
は指定した桁数で小数を切り捨てるメソッド- https://docs.ruby-lang.org/ja/latest/method/Float/i/floor.html
p 291.4.floor(1) # => 291.4 (ok) p 291.4.floor(2) # => 291.39 (not ok) p 291.4.floor(3) # => 291.4 (ok) p 291.4.floor(4) # => 291.4 (ok) p 291.4.floor(5) # => 291.39999 (not ok) p 291.4.floor(6) # => 291.4 (ok)
- この問題は最新版で修正済み
[Bug #18044] unusual behavior with Arabic string in a Hash
- アラビア語の文字列を含む
Hash
でキーを検索すると常にnil
が返ってくる、というバグ報告
foo = {"arabic" => "ٱلتَّوْبَة"} p foo.keys # => ["arabic"] p foo["arabic"] # => nil p foo.values_at("arabic") => [nil] foo.fetch "arabic" # Raises error with - did you mean "arabic" ?
- どうして…、と思ったら上記の再現コードのキーに
U200e
が紛れているのが原因だったらしいU200e
は Left-to-Right Mark (LRM) と呼ばれる制御文字
- 実際に Vim でコードを貼り付けるとこんな感じ
- 制御文字を取り除いたら問題なく動作した
foo = {"arabic" => "ٱلتَّوْبَة"} p foo.keys # => ["arabic"] p foo["arabic"] # => "ٱلتَّوْبَة" p foo.values_at("arabic") => {["ٱلتَّوْبَة"]=>[nil]}
[Feature #18040] Why should foo(1 if true)
be an error?
foo(1 if true)
がエラーになるのはなぜか?というバグ報告- パーサ的には
1 if true
は式ではなくてステートメントなので現状は意図する挙動だとコメントされている - その後は
1 if true
が式なのかステートメントなのかで議論が続いている - 最終的には『現実的に修正できない』という理由で Reject されている
- ちなみに
p foo((1 if true))
みたいに()
でくくると動作する
[Feature #15211] Integer.try_convert
Integer.try_convert
を追加する提案Array.try_convert
やRegexp.try_convert
などは既にある- 任意のオブジェクトを自身に変換するメソッド
Regexp.try_convert(/re/) # => /re/ # 失敗したら nil を返す Regexp.try_convert("re") # => nil
Integer.try_convert
は最新版では実装済み
p Integer.try_convert(10) # => 10 p Integer.try_convert("10") # => nil # ちなみに内部で to_int を呼び出しているので Float だとこうなる p Integer.try_convert(1.23) # => 1
[Feature #18008] keyword_init?
method for Struct
Struct.new
でkeyword_init: true
したかどうかを判定するメソッドを追加する提案- 次のように
Struct.new
の戻り値に対して判定できる
S1 = Struct.new(:a, :b) S2 = Struct.new(:a, :b, keyword_init: true) S3 = Struct.new(:a, :b, keyword_init: false) # 指定しなかった場合は nil を返す pp S1.keyword_init? # => nil pp S2.keyword_init? # => true pp S3.keyword_init? # => false
- これは既に最新版で対応済み
[Feature #17724] Make the pin operator support instance/class/global variables
- 元々はパターンマッチで
^@n
を使用すると『ローカル変数かメソッドを期待する』とエラーメッセージになっていた
# ローカル変数かメソッドを期待する、というエラーメッセージが出力される case 10 in ^@n end # => error: syntax error, unexpected instance variable, expecting local variable or method # in ^@n # ^~
- しかし、メソッドの場合でもエラーになるのでエラーメッセージを修正しよう、という内容のチケットだった
def n = 10 # メソッドを使用してもエラーになる case 10 in ^n end # => error: n: no such local variable
- これの対応として
^
にインスタンス/クラス/グローバル変数
が使えるように対応された
class X @a = 1 @@b = 2 $c = 3 # インスタンス、クラス、グローバル変数が使える case 1 in ^@a in ^@@b in ^$c end end
- これは最新版で実装済み
2021/07/18 今週の気になった bugs.ruby のチケット
今週は TracePoint 周りのバグがありました。
[Feature #17039] Remove Time#succ
Time#succ
を削除するチケット- 1.9.2 の頃から廃止の警告がでてたらしい
warning: Time#succ is obsolete; use time + 1
- と、いうわけで Ruby 3.1 から削除されるので使ってる場合は注意
[Bug #17945] Date::Infinity comparison <=> with Float::INFINITY not symmetric
Date::Infinity <=> Float::INFINITY
が対称でないというバグ報告
require 'date' p Float::INFINITY <=> Date::Infinity.new # => 0 p Date::Infinity.new <=> Float::INFINITY # => 1
- これは最新版では修正済み
- 両方共
0
を返す - https://github.com/ruby/date/pull/34
- 両方共
[Bug #18031] Nested TracePoint#enable with target crashes
- 以下のように
TracePoint
がネストしているとクラッシュするというバグ報告- どうして…
one = TracePoint.new(:call) {} two = TracePoint.new(:call) {} obj = Object.new obj.define_singleton_method(:foo) {} # a bmethod foo = obj.method(:foo) # ここでクラッシュする one.enable(target: foo) do two.enable(target: foo) {} end
- 修正 PR は既にあるぽい
- これとは別に2つの問題もあるらしい
- 1つは特定のコードでメモリリークする
# コード例 loop do tp = TracePoint.new(:call){} tp.enable tp.disable end
- もう1つは複数の
TracePoint
を使うと2つ目のTracePoint
が1つ目のTracePoint
を上書きするらしい
[PR #4638] Fix infinite loop when b_return TracePoint throws
- 次のように
TracePoint
のb_return
内で例外が発生すると無限ループになるバグの修正スクリプト- どうして…どうして…
class Foo define_singleton_method(:foo) { return } end TracePoint.trace(:b_return) do |tp| p tp raise end Foo.foo
- 6日前に PR が立ってるけど特に進展がなさそう