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 されている

[Feature #18351] Support anonymous rest and keyword rest argument forwarding

  • 以下のように無名な引数を他のメソッドにフォワードする提案
def foo(*)
  bar(*)
end

def baz(**)
  quux(**)
end
$ 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
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

[Bug #18378] Parsing CSV files from ARGF not working correctly anymore (it stopped working starting from Ruby 2.5)

  • CSV.newARGF を渡した時に正しく動作しないというバグ報告
    • Ruby 2.5 からバグってる
# file1.csv と file2.csv の両方のファイルを読み込んでほしいが file1.csv しか読み込まれない
ruby -r csv -e 'CSV.new(ARGF).each{|row| p row}' file1.csv file2.csv

[PR 5093] Move structs to classes, ~1.10x faster Ripper.lex

  • Ripper#Lexer で使用されている Struct を素のクラスに置き換えるとパフォーマンスが上がる PR
  • Struct と素のクラスで以下のようなパフォーマンスの違いがある
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
def initialize(i) super(i, Ripper.lex_state_name(i)).freeze end