2021/12/02 今回の気になった bugs.ruby のチケット
今週は Strcut
を素の class
に置き換えることで Ripper.lex
を高速化する提案がありました。
[Feature #18369] users.detect(:name, "Dorian") as shorthand for users.detect { |user| user.name == "Dorian" }
users.detect { |user| user.name == "Dorian" }
のショートハンドとしてusers.detect(:name, "Dorian")
とかけるようにする提案- ナンパラを使うと
users.detect { _1 == "Dorian" }
みたいに短くできるがそれよりももっと短く書きたいらしい - 他にも
all?
を(collection.all?(:attribute, value))
とかけるようにするとか - 他の書き方として
users.detect(&:name, "Dorian")
みたいにブロック引数を最初に書いたりとか - 既存の書き方でも
Proc
を生成するメソッドを用意すれば以下のようにかける、とコメントされている
def attreql k, v Proc.new{_1.send(k) == v} end class A attr_reader :foo, :bar def initialize foo, bar @foo, @bar = foo, bar end end collection = [A.new(1, 2), A.new(3, 4), A.new(5, 6)] collection.detect(&attreql(:foo, 3)) # => #<A:0x00007fb751064630 @foo=3, @bar=4> collection.all?(&attreql(:bar, 7)) # => false
- 以下のようにもかけるのでこちらの方が柔軟に対応できる
def attrlt k, v Proc.new{_1.send(k) < v} end collection.detect(&attrlt(:foo, 3)) # => #<A:0x00007fd3ab8a4680 @foo=1, @bar=2> collection.all?(&attrlt(:bar, 7)) # => true
- わたしもコメントされているような書き方の方が柔軟性が高くて引数が複雑にならなくていいなあ、と思う
[Feature #18366] Enumerator#return_eval
- 以下のようにイテレーションする際に元の値ではなくて評価した値を受け取りたいことがある
a = ["Hello", "my", "name", "is", "Ruby"] # 一番多い文字数を取得する a.max_by(&:length).length # => 5 # または a.map(&:length).max # => 5
- このような時にブロックの評価した値を返す
Enumerator#return_eval
を追加する提案
a = ["Hello", "my", "name", "is", "Ruby"] a.max_by.return_eval(&:length) # => 5 a.min_by.return_eval(&:length) # => 2 a.minmax_by.return_eval(&:length) # => [2, 5] ["Ava Davidson", "Benjamin Anderson", "Charlie Baker"] .sort_by.return_eval{_1.split.reverse.join(", ")} # => ["Anderson, Benjamin", "Baker, Charlie", "Davidson, Ava"]
- キーワード引数で制御する案もコメントされている
module TransducerSelect refine Array do def select(acc: [], step: :<<, step_eval: false) unless block_given? return to_enum(__method__) { size if respond_to?(:size) } end each do yielded = yield _1 step_value = step_eval ? yielded : _1 acc.public_send(step, step_value) if yielded end acc end end end using TransducerSelect ["Ms. Foo", "Dr. Bar", "Baz"].select(step_eval: true){_1[/\b[A-Z]\w+\./]} #=> ["Ms.", "Dr."] ["Ms. Foo", "Dr. Bar", "Baz"].select acc: Set.new, step: :add, step_eval: true do _1[/\b[A-Z]\w+\./] end #=> #<Set: {"Ms.", "Dr."}> ["Ms. Foo", "Dr. Bar", "Baz"].select acc: $stdout, step: :puts, step_eval: true do _1[/\b[A-Z]\w+\./] end #>> Ms. #>> Dr. #=> #<IO:<STDOUT>>
- 個人的にはよさそうな気がしたんですが以下の理由で Reject されている
- There's no real-world use case shown
- the term eval is not sufficient (unless it works as eval)
- return is even worse
- https://bugs.ruby-lang.org/issues/18366#note-7
[Feature #18351] Support anonymous rest and keyword rest argument forwarding
- 以下のように無名な引数を他のメソッドにフォワードする提案
def foo(*) bar(*) end def baz(**) quux(**) end
- [Feature #11256] anonymous block forwarding と同じようなチケット
- ブロック引数がフォワードできるならこっちもできるよね?と
- PR は既に投げられている
- ただし、この場合は
Method#parameters
で問題になると指摘されている...
の場合のみ[:rest, :*]
が生成されることが期待される- https://bugs.ruby-lang.org/issues/18351#note-1
$ ruby -e 'p method(def m(...); end).parameters' [[:rest, :*], [:block, :&]] $ ruby -e 'p method(def m(*); end).parameters' [[:rest]]
[Feature #12084] Class#instance
Array.singleton_class.instance # => Array "foo".singleton_class.instance # => "foo"
- また特異クラスでないオブジェクトから呼び出すとエラーになる
Array.instance # => error
- これは稀に欲しくなるのでほしいなあ
- 最近特異クラスから元のインスタンスのクラスを取得したい事があった
[Bug #18375] Timeout.timeout(sec, klass: MyExceptionClass) can not retry correctly.
Timeout.timeout
には第二引数にタイムアウトしたときの例外クラスを指定できる- その時に以下のコードがうまく動作しないというバグ報告
require "timeout" class DelayError < Exception end Timeout.timeout(2, DelayError) do |arg| puts 'start' sleep 10 rescue DelayError puts '*'*10 retry end __END__ # 実際の出力 start ********** start # 期待する出力 start ********** start ********** start ********** ...
- これは上のコードが下のコードとして解釈されるので例外をキャッチする場所が意図する場所じゃないため
Timeout.timeout(2, DelayError) do |arg| # Timeout.timeout が投げる例外じゃなくてブロックの中の処理に対してキャッチする begin puts 'start' sleep 10 rescue DelayError puts '*'*10 retry end end
- 期待する挙動としては以下のように
Timeout.timeout
全体で例外をキャッチする必要がある
begin Timeout.timeout(2, DelayError) do |arg| puts 'start' sleep 10 end rescue DelayError puts '*'*10 retry end
[Bug #18377] Integer#times has different behavior depending on the size of the integer
Integer#+
を書き換えると特定の値でInteger#times
の挙動に影響を与えるというバグ報告
# これは問題がない (2**1).times do Integer.undef_method(:+) Integer.define_method(:+) do |_other| puts "my custom add" end end # FIXNUM を越える値に対して `times` を呼び出すと Integer#+ を呼び出してエラーになる # `times': undefined method `<' for nil:NilClass (NoMethodError) (2**65).times do Integer.undef_method(:+) Integer.define_method(:+) do |_other| # ここが呼び出されるようになる puts "my custom add" end end
- 内部で
Integer#+
を呼び出すようになっていたところを呼び出さないようにして対応済み
[Bug #18378] Parsing CSV files from ARGF not working correctly anymore (it stopped working starting from Ruby 2.5)
CSV.new
にARGF
を渡した時に正しく動作しないというバグ報告- Ruby 2.5 からバグってる
# file1.csv と file2.csv の両方のファイルを読み込んでほしいが file1.csv しか読み込まれない ruby -r csv -e 'CSV.new(ARGF).each{|row| p row}' file1.csv file2.csv
ARGF
なんていうオブジェクトがあったんですね、知らんかった
[PR 5093] Move structs to classes, ~1.10x faster Ripper.lex
Ripper#Lexer
で使用されているStruct
を素のクラスに置き換えるとパフォーマンスが上がる PRStruct
と素のクラスで以下のようなパフォーマンスの違いがある
Elem = Struct.new(:pos, :event, :tok, :state, :message) do def initialize(pos, event, tok, state, message = nil) super(pos, event, tok, State.new(state), message) end # ... def to_a a = super a.pop unless a.empty? a end end class ElemClass attr_accessor :pos, :event, :tok, :state, :message def initialize(pos, event, tok, state, message = nil) @pos = pos @event = event @tok = tok @state = State.new(state) @message = message end def to_a if @message [@pos, @event, @tok, @state, @message] else [@pos, @event, @tok, @state] end end end # stub state class creation for now class State; def initialize(val); end; end require 'benchmark/ips' require 'ripper' pos = [1, 2] event = :on_nl tok = "\n".freeze state = Ripper::EXPR_BEG puts "インスタンスを生成するベンチマーク" Benchmark.ips do |x| x.report("struct") { Elem.new(pos, event, tok, state) } x.report("class ") { ElemClass.new(pos, event, tok, state) } x.compare! end puts puts "---" * 10 puts struct = Elem.new(pos, event, tok, state) from_class = ElemClass.new(pos, event, tok, state) puts "配列に変換するベンチマーク" Benchmark.ips do |x| x.report("struct") { struct.to_a } x.report("class ") { from_class.to_a } x.compare! end puts puts "---" * 10 puts puts "要素へアクセスするベンチマーク" Benchmark.ips do |x| x.report("struct") { struct.pos[1] } x.report("class ") { from_class.pos[1] } x.compare! end __END__ output: インスタンスを生成するベンチマーク Warming up -------------------------------------- struct 422.575k i/100ms class 473.724k i/100ms Calculating ------------------------------------- struct 4.217M (± 0.8%) i/s - 21.129M in 5.010712s class 4.790M (± 0.9%) i/s - 24.160M in 5.044092s Comparison: class : 4790159.6 i/s struct: 4217002.8 i/s - 1.14x (± 0.00) slower ------------------------------ 配列に変換するベンチマーク Warming up -------------------------------------- struct 990.000k i/100ms class 1.291M i/100ms Calculating ------------------------------------- struct 9.726M (± 2.1%) i/s - 49.500M in 5.091785s class 12.502M (± 1.9%) i/s - 63.238M in 5.060281s Comparison: class : 12501800.1 i/s struct: 9726043.7 i/s - 1.29x (± 0.00) slower ------------------------------ 要素へアクセスするベンチマーク Warming up -------------------------------------- struct 2.358M i/100ms class 2.559M i/100ms Calculating ------------------------------------- struct 23.080M (± 1.1%) i/s - 115.563M in 5.007690s class 24.767M (± 2.0%) i/s - 125.375M in 5.064515s Comparison: class : 24766677.1 i/s struct: 23080029.6 i/s - 1.07x (± 0.00) slower
- Ripper の実装のボトルネックは以下のように
super
を呼び出している部分ぽい?
def initialize(i) super(i, Ripper.lex_state_name(i)).freeze end
- これに関連して
Struct
のアクセスを最適化する PR も投げられていてこちらはマージされている - 今見たらこの PR 自体もマージされていた