2020/12/24 今週の気になった bugs.ruby のチケット

内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。

[Bug #17030] Enumerable#grep{_v} should be optimized for Regexp その後

  • 以前紹介した ary.select { |e| e.match?(reg) } と比較して ary.grep(reg) の方が遅いので最適化しよう、という提案
  • その後、議論が進んで最終的に ary.grep(reg) のようにブロック引数がない場合は MatchData を生成しないように対応された
ary = ["homu", "mami", "mado"]

reg = /.*/
ary.grep(reg)
# or Regexp.last_match
p $~
# 2.7 => #<MatchData "mado">
# 3.0 => nil

# ブロックを渡した場合は MatchData を生成する
ary.grep(reg) {}
# or Regexp.last_match
p $~
# 2.7 も 3.0 => #<MatchData "mado">
  • この挙動は非互換な変更になるので注意する必要がります

[PR #124] Add measure command

  • irbmeasure というコマンドが追加された
  • これは measure を呼び出した以降で実行時間を出力するような事を行う
irb(main):001:0> 3
 => 3
irb(main):002:0> measure
TIME is added.
 => nil
irb(main):003:0> 3
processing time: 0.000058s
 => 3
irb(main):004:0> measure :off
 => nil
irb(main):005:0> 3
 => 3
  • また以下のようにカスタマイズすることも可能です
IRB.conf[:MEASURE_PROC][:CUSTOM] = proc { |context, code, line_no, &block|
  time = Time.now
  result = block.()
  now = Time.now
  puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE]
  result
}
  • これは普通に便利そう

[Bug #17428] Method#inspect bad output for class methods

  • Method#inspect の内容がおかしいというバグ報告
  • 次のようにクラス名が表示されないケースがある
p String.method(:prepend)
# 2.7 => #<Method: String.prepend(*)>
# 3.0 => #<Method: #<Class:Object>(Module)#prepend(*)>
  • この問題は最新版では以下のように修正された
p String.method(:prepend)
# 2.7 => #<Method: String.prepend(*)>
# 3.0 => #<Method: #<Class:String>(Module)#prepend(*)>
  • これは気づかなかった…

[Feature #17116] raise ArgumentError in Enumerator#new in no given blocks

  • Enumerator.new(obj) みたいに .new にブロックを渡さない場合は deprecated warning が出ている
    • この警告がでているのは Ruby 2.0 の頃から
obj = Object.new
# warning: Enumerator.new without a block is deprecated; use Object#to_enum instead
Enumerator.new(obj)
  • これはもうエラーにしてしまってもいいんじゃないか、というチケット
  • これはマージされて Ruby 3.0 からはエラーになるので注意しましょう

[Bug #17423] Prepend should prepend a module before the class

  • Ruby 3.0 では以下のように prepend の挙動が変わってしまう事がある
module M; end
module A; end
class B; include A; end

A.prepend M
B.prepend M

# B.prepend M してるのに M が B よりもあとに来る
p B.ancestors
# 2.7 => [M, B, A, Object, Kernel, BasicObject]
# 3.0 => [B, M, A, Object, Kernel, BasicObject]
  • これを次のように M が重複するようにするという内容のチケット
module M; end
module A; end
class B; include A; end

A.prepend M
B.prepend M

# このように修正する
p B.ancestors
# => [M, B, M, A, Object, Kernel, BasicObject]
  • この問題は Ruby 3.0 リリース後に対応される予定です
  • ちなみにこのチケットはまつもとさん自身が建てています

[Feature #17411] Allow expressions in pattern matching

  • パターンマッチでは次のようにパターンの部分に式を書くことができません
user = { name: "homu", age: 14 }
case user
# syntax error, unexpected '+', expecting ')'
# パターンに式を記述する事ができない
in { age: (7 + 7) }
end
  • これを ^(expression) のようにかけるようにしようという提案
user = { name: "homu", age: 14 }
case user
# OK
# 式を書く場合は ^() を使う
in { age: ^(7 + 7) }
end

[Bug #17398] SyntaxError in endless method

  • Ruby 3.0 のエンドレスメソッド定義で次のような定義の場合シンタックスエラーになる、という旨のチケット
# OK
def hoge = puts("homu")

# syntax error, unexpected string literal, expecting `do' or '{' or '('
def hoge = puts "homu"

【一人 bugs.ruby Advent Calendar 2020】[Bug #17423] `Prepend` should prepend a module before the class【24日目】

一人 bugs.ruby Advent Calendar 2020 24日目の記事になります。

[Bug #17423] Prepend should prepend a module before the class

このブログでも何回か紹介しているんですが Ruby 3.0 では Module#include / #prepend の挙動がちょっと変わります。
特に以下のように Ruby 2.7 と Ruby 3.0 でかなり挙動が変わるコードも存在しています。

module M; end
module A; end
class B; include A; end

A.prepend M
B.prepend M

# B.prepend M してるのに M が B よりもあとに来る
p B.ancestors
# 2.7 => [M, B, A, Object, Kernel, BasicObject]
# 3.0 => [B, M, A, Object, Kernel, BasicObject]

このチケットはこの問題を解決するために以下のように M が重複することを許容しよう、という旨のチケットになります。

module M; end
module A; end
class B; include A; end

A.prepend M
B.prepend M

# このように修正する
p B.ancestors
# => [M, B, M, A, Object, Kernel, BasicObject]

これはこれでちょっと気持ち悪い気もするんですがまあ無いよりはマシですかねえ…ぶっ壊れるときはどっちにしてもぶっ壊れそうだし…。
この問題は Ruby 3.0 リリース後に対応される予定となっています。
ちなみにこのチケットはまつもとさん自身が建てていたりします。

【一人 bugs.ruby Advent Calendar 2020】[Feature #16986] Anonymous Struct literal【23日目】

【一人 bugs.ruby Advent Calendar 2020】[Feature #16986] Anonymous Struct literal【23日目】 一人 bugs.ruby Advent Calendar 2020 23日目の記事になります。

[Feature #16986] Anonymous Struct literal

これは Struct.new(:a, :b).new(1, 2)${ a: 1, b: 2 } のようなリテラルで定義できるようにするチケットです。

s = ${a: 1, b: 2, c: 3}
s.a  # => 1
s.b  # => 2
s.c  # => 3

いまはそんなに Struct は使わないけどこういう記法があるとガンガン使いそうですねー。
例えばこんな感じに雑にダックタイピング呼び出しするメソッドに値を渡す場合とか?

def print(user)
  pp "#{user.id} #{user.name}"
end

name = "homu"
age = 14
# Struct を経由してメソッド呼び出しされるようにする
print(${ name: name, age: age })

Struct だと obj.value だけじゃなくて obj[:value] みたいに添え字アクセスもできるので Hash の代わりとして使用できそうですね。
Hash と違い存在しないキーにアクセスするとエラーになるのは便利そう

# Hash の場合は typo してても気づきづらい
user = { name: "homu", age: 14 }
# no error
user[:nmae]

# Struct だと存在しないキーにアクセスするとエラーになる
user = Struct.new(:name, :age).new("homu", 14)
# error
user[:nmae]

あとは ${} だとブロックと差別化できるので p { a: 1, b: 2 } とは書けないんですが p ${ a: 1, b: 2 } はかける的な。
関係ないけどどういう経緯でチケットが建てられたのか見ているとちょっとおもしろいです

Struct に関してはこちらのスライドも参照してください。

[今更聞けない! Struct の使い方と今後の可能性について]

【一人 bugs.ruby Advent Calendar 2020】[Feature #17004] Provide a way for methods to omit their return value【22日目】

一人 bugs.ruby Advent Calendar 2020 22日目の記事になります。

[Feature #17004] Provide a way for methods to omit their return value

このチケットは任意のメソッドが戻り値を受け取るか受け取らないかを判定するメソッドの追加を追加するという内容のチケットです。
ということかというと RubyVM.return_value_is_used? というメソッドを追加し、これをメソッド内で呼び出すと

  • 戻り値を受け取る場合: true を返す
  • そうでない場合: false を返す

というような判定を行うことができます。
具体的に言うとこんな感じで判定する事ができます。

def hoge
  if RubyVM.return_value_is_used?
    pp "戻り値を受け取る"
  else
    pp "戻り値を受け取らない"
  end
end

hoge          # "戻り値を受け取らない"
value = hoge  # "戻り値を受け取る"
Array hoge    # "戻り値を受け取る"
hoge.nil?     # "戻り値を受け取る"

# 最後に呼び出したやつも?
hoge          # "戻り値を受け取る"

この判定メソッドを利用すると次のように『戻り値を受け取らない場合は無駄な処理を省く』事ができます。

class Hash
  def refresh(key)
    # 引数を受け取る場合のみ result を設定する
    if RubyVM.return_value_is_used?
      result = self[key]
    end
    self[key] = nil
    result
  end
end

homu = { name: "homu", age: 14 }
homu.refresh(:name)
p homu
# => {:name=>nil, :age=>14}

age = homu.refresh(:age)
pp homu  # => {:name=>nil, :age=>nil}
pp age   # => 14
class User < ActiveRecord::Base
  def update_name(name)
    update!(name: name)

    # reload した値を返す
    reload if RubyVM.return_value_is_used?
  end
end

これは面白いアプローチですね。
ただし、次のように戻り値になる場合は『戻り値を受け取る』ことになるので注意。

def hoge
  if RubyVM.return_value_is_used?
    pp "戻り値を受け取る"
  else
    pp "戻り値を受け取らない"
  end
end

def foo
  hoge
end
foo    # "戻り値を受け取る"

def bar
  hoge
  nil
end
bar    # "戻り値を受け取らない"

便利そうっちゃ便利そうだけどメソッドごとに RubyVM.return_value_is_used? で処理を分岐するのはめっちゃきつそう…。
実際には極端に重くなるようなメソッドぐらいで使いそうな気がするけど…どうだろうか。

参照

【Ruby 3.0 Advent Calendar 2020】Ruby 3.0 で WEBrick と SDBM が標準ライブラリから削除される【22日目】

Ruby 3.0 Advent Calendar 2020 22日目の記事になります。
この前の銀座Rails で完全に話忘れてたのでここに書いておきます。

Ruby 3.0 で WEBrick と SDBM が標準ライブラリから削除される

表題のとおりですが Ruby 3.0 から標準ライブラリの WEBrick と SDBM が削除されます。

どちらも削除される主な理由としてはサポートするのが難しくなってきたからとのことです。
特に WEBrick は最近脆弱性の問題もありました。
今後は ruby/sdbmruby/webrick でメンテされていくようです。
SDBM や WEBrick を使用したい場合は明示的に gem install したり Gemfile に記述する必要があります。
この影響に対してすでに Rails は対処済みです。

WEBrick は特に利用している人もそれなりにいそうな気がするので Ruby 3.0 での影響は大きそうですねえ。

【一人 bugs.ruby Advent Calendar 2020】[Feature #17292] id outputed by inspect and to_s output does not allow to find actual object_id and vice-versa【21日目】

一人 bugs.ruby Advent Calendar 2020 21日目の記事になります。

[Misc #17199] id outputed by inspect and to_s output does not allow to find actual object_id and vice-versa

Ruby 2.7 から #inspect ( #to_s ) と #__id__ が返すアドレスの関連性が一致しなくなったという報告チケットなります。
どういうことかというと Ruby 2.7 から以下のように #__id__ が返す値が変わりました。

obj = Object.new

# Ruby 2.7 から #__id__ の値が変わった
p "#__id__=#{obj.__id__}"
# Ruby 2.6 => "#__id__=47458875463160"
# Ruby 2.7 => "#__id__=60"

これと #inspect がどう関係あるのかというと #__id__ から #inspect で表示されるアドレスを推論できていたんですが、それが上記の変更でできなくなりました。

obj = Object.new
p "#inspect=#{obj.inspect}"
# => "#inspect=#<Object:0x00005653c2d3abf0>"

# Ruby 2.6 では __id__ の結果をシフトすると inspect に表示される id と同じになっていた
# しかし Ruby 2.7 ではできなくなっている
p "shifted_id=#{(obj.__id__ << 1).to_s(16)}"
# Ruby 2.6 => "shifted_id=5653c2d3abf0"
# Ruby 2.7 => "shifted_id=78"

これにより何が起きるのかというと例えば #inspect の出力の 0x00005653c2d3abf0 というアドレスから __id__ を推論する事ができたので次のように ObjectSpace._id2ref で実際のオブジェクトの値を取得する事ができていました。
しかし、 Ruby 2.7 の変更によりこれはできなりました。

o = Object.new
# 任意の __id__ からそのオブジェクトが取得できる
pp ObjectSpace._id2ref(o.__id__)

# inspect から id を取得してそれを元にしてオブジェクトを取得する事ができた
# これが Ruby 2.7 からは動作しなくなっている
id_from_inspect = o.inspect[/#<Object:(.*)>/, 1].to_i(16)
pp ObjectSpace._id2ref(id_from_inspect >> 1)

こんな仕様があったんですね…。
例えば Object#inspect をログ出力しているような場合にその出力されたアドレスから『実際のオブジェクトを参照したい』みたいな場合に利用できるのかなあ、とは思いました。

【一人 bugs.ruby Advent Calendar 2020】[Bug #17017] Range#max & Range#minmax incorrectly use Float end as max【20日目】

一人 bugs.ruby Advent Calendar 2020 20日目の記事になります。

誤解しないように最初に書いておくとこのチケットによる Ruby 3.0 への影響はありません。

[Bug #17017] Range#max & Range#minmax incorrectly use Float end as max

このチケットは以下のように range.maxrange.to_a.max で差異があり一貫性がないので対処しよう、という旨のチケットです。

# # これは期待する挙動
(1..3.1).to_a == [1, 2, 3]

# to_a を経由した場合の結果
(1..3.1).to_a.max    == 3
(1..3.1).to_a.minmax == [1, 3]

# Range#max Range#minmax
(1..3.1).max    == 3.1
(1..3.1).minmax == [1, 3.1]

これは最初は以下のように range.max == range.to_a.max となるように修正されました。

(1..3.1).max     # => 3
(1..3.1).minmax  # => [1, 3]

Range#max の変更により既存のコードが壊れた

Range#max の戻り値を変えたことにより以下のような非互換な挙動が発生していました。
NOTE: ちなみに 2.8.0 というのは当時はまだバージョニングが 3.0 ではなかった名残です。

# 2.7.1    : Infinity を返す
# 2.8.0dev : error: `floor': Infinity (FloatDomainError)
p (42..Float::INFINITY).max

# 明示的に Float に変換すると OK
p (42.to_f..Float::INFINITY).max

この非互換な変更により RuboCopRails が壊れたという報告がされました。

Float::INFINITY の場合に非互換にしないようにした

先程の (42..Float::INFINITY).max が非互換な挙動になってしまう為、非互換にならないような対応がなされました。

p (42..Float::INFINITY).max
# 2.7.1 : => Infinity
# 2.8.0dev : => Infinity

これにより件の非互換な変更に対する問題は対処されました。

最終的には全て Revert され変更はなくなった

さて、いろいろと紆余曲折があったこのチケットですが最終的にはまつもとさんの意向により全て Revert されました。
意図としては、

  • Range の役割としては『 Enumerable としての機能』と『両端のデータを持つオブジェクトとしての機能』の 2パターンがある
  • 今回の #max #minmax は『両端のデータを返す』というのが期待する挙動となる
  • なので (1..3.5).max は終端の 3.5 を返すのは意図する動作になる

とのことでした。
長々と議論されたチケットですがこのチケットによる Ruby 3.0 へ影響はありません。
Ruby はこんな感じで紆余曲折あり開発されているというのがわかるチケットでした。