2021/07/08 今週の気になった bugs.ruby のチケット
今週は Ruby 3.1.0 で default gem から bundled gem に移動するチケットなどを載せています。
[Feature #17873] Update of default gems in Ruby 3.1
- Ruby 3.1 からいくつかの default gem が bundled gem に変更されるチケット
- 現時点で(あくまでも現時点で)は以下の default gem が bundled gem に変更されるので注意する必要がある
- また以下の gem は default gem から取り除かれます
- tracer
- dbm
- gdbm
- default gem と bundled gem の違い
- default gem
- Ruby 本体にバンドルされている gem
- gem 単体で更新可能
- gem 自体を削除するのは不可能
- bundler 環境でも使用可能
- bundled gem
- Ruby 本体にバンドルされている gem
- gem 単体で更新可能
- gem 自体を削除するのは可能
- bundler 環境では使用不可能
- 使用する場合は明示的に
Gemfile
にgem "xxx"
と書いておく必要がある
- 使用する場合は明示的に
- Ruby 本体にバンドルされている gem
- bundled gem と default gem の違いの具体例 - @znz blog
- default gem
- ちなみにこの影響で capypara などが壊れたらしい
[Bug #16243 完了] case/when is slower than if on MRI
- Ruby 2.6.5 で if 文よりも case when 文の方が遅いというバグ報告
# frozen_string_literal: true require "benchmark/ips" def deep_dup_case(obj) case obj when Integer, Float, TrueClass, FalseClass, NilClass obj when String obj.dup when Array obj.map { |e| deep_dup_case(e) } when Hash duped = obj.dup duped.each_pair do |key, value| duped[key] = deep_dup_case(value) end else obj.dup end end def deep_dup_if(obj) if Integer === obj || Float === obj || TrueClass === obj || FalseClass === obj || NilClass === obj obj elsif String === obj obj.dup elsif Array === obj obj.map { |e| deep_dup_if(e) } elsif Hash === obj duped = obj.dup duped.each_pair do |key, value| duped[key] = deep_dup_if(value) end duped else obj.dup end end obj = { "class" => "FooWorker", "args" => [1, 2, 3, "foobar"], "jid" => "123987123" } Benchmark.ips do |x| x.report("deep_dup_case") do deep_dup_case(obj) end x.report("deep_dup_if") do deep_dup_if(obj) end x.compare! end
Warming up -------------------------------------- deep_dup_case 37.767k i/100ms deep_dup_if 41.802k i/100ms Calculating ------------------------------------- deep_dup_case 408.046k (± 0.9%) i/s - 2.077M in 5.090997s deep_dup_if 456.657k (± 0.9%) i/s - 2.299M in 5.035040s Comparison: deep_dup_if: 456657.4 i/s deep_dup_case: 408046.1 i/s - 1.12x slower
- 最新版では改善されているそうなのでシュッと閉じられている
2021/07/01 今週の気になった bugs.ruby のチケット
今週はエラー箇所をハイライトする error_highlight
という gem が本体に追加されました。
[Feature #17930] Add column information into error backtrace
- エラー箇所をハイライトする機能の提案
error_highlight
という gem として取り込まれた- Ruby 3.1.0 以降では以下のようにエラー箇所が
^
でハイライトされるようになった
$ ruby -e '"1234".times { }' -e:1:in `<main>': undefined method `times' for "1234":String (NoMethodError) "1234".times { } ^^^^^^ $
- ただし、エラー行にマルチバイト文字や一部の記号が含まれている場合うまくハイライトされないので注意
$ ruby -e '1.あいうえお {}' -e:1:in `<main>': undefined method `あいうえお' for 1:Integer (NoMethodError) 1.あいうえお {} ^^^^^^ $
$ ruby -e '"\"#{1234}\"".times { }' -e:1:in `<main>': undefined method `times' for "\\"1234\\"":String (NoMethodError) "\\"#{1234}\\"".times { } ^^^^^^ $
- つらいので直したいが直すのもつらい…
[Feature #17881] Add a Module#const_added callback
2021/06/24 今週の気になった bugs.ruby のチケット
今週はエラー箇所をマークする gem の PR などがありました。
[Bug #14817] TracePoint#parameters for bmethod's return event should return the same value as its Method#parameters
- TracePoint の
:return
イベント時にTracePoint#parameters
で正しく値が取得できないバグ報告
define_method(:bm) {|a|} p method_parameters: method(:bm).parameters # => {:method_parameters=>[[:req, :a]]} trace = TracePoint.new(:call, :return){|tp| mid = tp.method_id if mid == :bm p mid: mid, event: tp.event, tp_parameters: tp.parameters end } trace.enable{ bm(0) } # :call 時は parameters が取得できているが # :return 時は parameters が取得できてない # output: # {:mid=>:bm, :event=>:call, :tp_parameters=>[[:req, :a]]} # {:mid=>:bm, :event=>:return, :tp_parameters=>[]}
TracePoint#parameters
だけではなくてdefine_method
+TracePoint
全般の問題らしい
define_method(:bm) {|a|} trace = TracePoint.new(:call, :return){|tp| p [tp.event, tp.lineno] if tp.method_id == :bm } trace.enable{ bm(0) } # output: # [:call, 1] # [:return, 7] #=> [:return, 1] になるべき?
- チケット自体は3年前のやつだけど最近修正 PR が投げられてた
[Bug #14391] Integer#digitsが遅い
Integer#digits
が遅いというバグ報告Integer#to_s
と比較してもかなり遅いらしい
(9999**9999).to_s.chars.map(&:to_i).reverse # 0.030225秒 (9999**9999).digits # 1.187126秒 (40倍) (99999**99999).to_s.chars.map(&:to_i).reverse # 1.888218秒 (99999**99999).digits # 195.594539秒 (100倍)
Integer#digits
は各桁を配列として返すメソッド
pp 16.digits # => [6, 1] pp 1234.digits # => [4, 3, 2, 1]
- 最近 PR が投げられてた
[PR 4414] [WIP] add error_squiggle gem
- エラー箇所をマークする gem が開発されてるよ、っていう話
- #17930 関連
$ ./local/bin/ruby -e '1.time {}' -e:1:in `<main>': undefined method `time' for 1:Integer (NoMethodError) 1.time {} ^^^^^ Did you mean? times
- gem 名についての意見を求めている
- ちなみにエラー箇所にマルチバイト文字が含まれているとぶっ壊れるのでなんとかしてえ
$ ./ruby -e 'ああああ.time {}' -e:1:in `<main>': undefined local variable or method `ああああ' for main:Object (NameError) ああああ.time {} ^^^^^^^^^^^^
$ ./ruby -e '1.あああ {}' -e:1:in `<main>': undefined method `あああ' for 1:Integer (NoMethodError) 1.あああ {} ^^^^
- 原因自体はわかっているんだけどマルチバイト文字で表示幅を計算する方法が現状 reline の内部 API にしか存在していないのでつらい
[Feature #18004] Add Async to the stdlib
- async-gem を標準ライブラリに追加しよう!というチケット
require 'async' def sleepy(duration = 3) Async do |task| task.sleep duration puts "I'm done sleeping, time for action!" end end # 同期処理 sleepy # 非同期処理 Async do # 同時に sleep が実行される sleepy sleepy end
2021/06/17 今週の気になった bugs.ruby のチケット
今週は ENV
に対して変更が入りました。
[Bug #17098] Float#negative? reports negative zero as not negative
-0.0
がFloat#negative?
でfalse
を返すのは期待しているのか?というバグ報告- IEEE 754 の規格?でそう定まってるらしい?
neg_zero = -0.0 pp neg_zero.negative? # => false pp neg_zero < 0 # => false
- チケットを立てた人が期待する動作としてはこう
-0.0.negative? # => true 0.0.positive? # => false
- その他、現状の挙動
pp RUBY_VERSION # => "3.0.1" # これは両方共 true pp 0.0 == 0.0 # => true pp -0.0 == -0.0 # => true # equal? の場合 pp 0.0.equal?(0.0) # => true pp -0.0.equal?(-0.0) # => false # 変数に代入してから比較すると a = -0.0 pp a.equal?(a) # => true puts "---" # -0.0 か判定する方法 # https://bugs.ruby-lang.org/issues/17098#note-7 def check_negative_zero(f) 1.0 / f == -Float::INFINITY end pp check_negative_zero(-0.0) # => true pp check_negative_zero(0.0) # => false
- また
Float#signbit
を追加するようなコメントをされている - 実際
0.0
と-0.0
を区別したいユースケースってあるんですかね
[Feature #17950] Unable to pattern-match against a String key
- パターンマッチでキーが
String
の場合にエラーになるというバグ報告
# Hash のキーに文字列がある場合、マッチする事ができない case { status: 200, headers: {"content-type" => "application/json"}, body: "bla" } in { status: , headers: { "content-type" => type }, body: } end # syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END # ...tus: , headers: {"content-type" => type}, body: }
- これは現時点では仕様
- ちなみにこういう風に書くことは可能
- 可能なだけで書きたくはないなあ
case { status: 200, headers: {"content-type" => "application/json"}, body: "bla" } in { status: , headers: headers, body: } if type = headers["content-type"] pp type # => "application/json" end
- 早くパターンマッチ使いたい
[Bug #17767] Cloned ENV
inconsistently returns ENV
or self
ENV
とENV.clone
したオブジェクトで挙動に一貫性がないというバグ報告
cloned_env = ENV.clone p ENV.each_key{}.equal?(ENV) #=> true p cloned_env.each_key{}.equal?(cloned_env) #=> true ENV.delete('TEST') err = ENV.fetch('TEST') rescue $! p err.receiver.equal?(ENV) #=> true err = cloned_env.fetch('TEST') rescue $! p err.receiver.equal?(cloned_env) #=> false ENV['TEST'] = 'TRUE' p ENV.select!{ false }.equal?(ENV) #=> true cloned_env['TEST'] = 'TRUE' p cloned_env.select!{ false }.equal?(cloned_env) #=> false
- このチケットがきっかけで Ruby 3.1 から以下のように挙動が変わった
ENV.dup
を使うと例外が発生するENV.clone
を使うと警告がでる- 詳細はこちら : https://blog.n-z.jp/blog/2021-06-12-ruby-env.html
[Bug #17951] Collisions in Proc#hash values for blocks defined at the same line
Proc#hash
の値が同じ値になるケースがあるというバグ報告
require 'set' def capture(&block) block end # 同じブロックを大量に生成する blocks = Array.new(1000) { capture { :foo } } hashes = blocks.map(&:hash).uniq ids = blocks.map(&:object_id).uniq equality = blocks.map { blocks[0].eql?(_1) }.tally hash = blocks.to_h { [_1, nil] } set = blocks.to_set # hash が一意であれば hashes.size == 1000 になるはずだがなっていない puts(hashes.size) # => 11 puts(ids.size) # => 1000 puts(equality.inspect) # => {true=>1, false=>999} puts(hash.size) # => 1000 puts(set.size) # => 1000
- このバグはまだマージされていないが PR は作成され済み
[Bug #15993] 'require' doesn't work if there are Cyrillic chars in the path to Ruby dir
D:\users\киї\Ruby\2.6\bin>ruby -v ruby 2.6.3p62 (2019-04-16 revision 67580) [x64-mingw32] D:\users\киї\Ruby\2.6\bin>ruby -e "require 'logger'" Traceback (most recent call last): 1: from <internal:gem_prelude>:2:in `<internal:gem_prelude>' <internal:gem_prelude>:2:in `require': No such file or directory -- D:/users/РєРёС—/Ruby/2.6/lib/ruby/2.6.0/rubygems.rb (LoadError)
D:\Евгений>C:\Ruby26-x64\bin\ruby -I D:\Евгений -e "require 'logger'" Traceback (most recent call last): 2: from -e:1:in `<main>' 1: from C:/Ruby26-x64/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require' C:/Ruby26-x64/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require': No such file or directory -- D:/Евгений/logger.rb (LoadError) D:\Евгений>C:\Ruby27-x64\bin\ruby -I D:\Евгений -e "require 'logger'" D:\Евгений>C:\Ruby30-x64\bin\ruby -I D:\Евгений -e "require 'logger'"
- どうやって直ったのかが気になる…
2021/06/10 今週の気になった bugs.ruby のチケット
今週は TypeScript のようなコンストラクタを Ruby に導入したい、というチケットなどがありました。
[Feature #17942] Add a initialize(public @a, private @b)
shortcut syntax for defining public/private accessors for instance vars as part of constructor
- TypeScript だと以下のようにコンストラクタのショートハンドを記述できる
class Foo { constructor(public a:number, public b:number, private c:number) { } }
- 上記は以下と同じ意味
class Foo { constructor(a, b, c) { this.a = a; this.b = b; this.c = c; } }
class Thing def initialize(public @a, public @b, @c) end end
- 上記は以下と同じ意味
class Thing attr_accessor :a, :b def initialize(a, b, c) @a = a @b = b @c = c end end
- 個人的には強く
#initialize
に依存してしまっている機能なのでちょっとイマイチ#initialize
はただのコールバックメソッドに過ぎない- メソッドでしかないので他のメソッドでも使えないと一貫性がない
- 仮引数はどうやって参照するの?
- アクセッサまで定義されてるのは意味が強すぎる
- Ruby ではあとから
#initialize
も再定義できるのでその場合にどうなるのかが気になる
class Thing def initialize(public @a, public @b, @c) end end class Thing # こう再定義したらどうなるの? def initialize(@a, public @a) end end
- どうせやるなら宣言的にやったほうがいい気はする
- が、これやるなら gem でいいじゃんって思う
class Thing initialize(public: [:a, :b], private: :c) do end end
- あと
Struct
を使えばアクセシビリティは制御できないがもっと簡素にかける
class Thing < Struct.new(:a, :b, :c) end
- この手の話は定期的に出ている
[Feature #17938] Keyword alternative for boolean positional arguments
bool
値を渡している位置引数をキーワード引数に置き換えようという提案- 例えば以下のメソッドは
true / false
で挙動を制御できるがそれが何を意味しているのかがわからない
object.respond_to?(:symbol, false) # what does `false` mean? object.methods(true) # what does `true` mean?
- なので以下のようにキーワード引数にしようという提案
object.respond_to?(:symbol, include_all: false) object.methods(regular: true) # or object.methods(only_public: true) # or object.methods(include_all: false)
- 実装案は以下のように非互換にならないようするイメージ
def respond_to?(symbol, include_all_positional=false, include_all: nil) include_all ||= include_all_positional # ... end
- 引数を自明にしたいなら以下のように変数に代入すればいいのでは?とコメントされてる
- 流石に変数を定義する副作用がでかい…
- https://bugs.ruby-lang.org/issues/17938#note-4
obj = Object.new obj.respond_to?(:symbol, include_all = false) # or obj.methods(include_inherited = true)
- これは Ruby の問題じゃなくてエディタ側でカバーすべきでは?みたいなコメントもある
- RubyMine とかは仮引数まで含めて補完ができるっぽい
- https://bugs.ruby-lang.org/issues/17938#note-5
- キーワード引数にした場合のキーの名前をどうするのかとパフォーマンスの懸念点が上げられている
[Bug #17925] Pattern matching syntax using semicolon one-line
case expression in 42; end
とワンラインでパターンマッチを記述した場合にエラーになるのは意図する挙動なのか?というバグチケットcase expression when 42; end
みたいにwhen
だおt問題ないcase expression; in 42; end
みたいに;
で区切ると問題ない- これは
case (expression in 42); end
と解釈されてしまっているのが問題らしい
[Bug #17937] Segmentation fault in Enumerator#next on Apple M1, Mac OS Big Sur 11.2.2
- Ruby 2.7.1 + Apple M1, Mac OS Big Sur 11.2.2 で
[1,2,3].to_enum.next
を実行すると Segv するというバグ報告- どうして…
- Ruby 2.7.2 以降と 3.0.1 以降では直っている
[Feature #17930] Add column information into error backtrace
- 先週からの続き
- 現在は
Thread::Backtrace::Location
ではなくてRubyVM::AbstractSyntaxTree.of
で情報を取得する実装が進んでいる
def target # caller_locations[0] は自身のメソッドの呼び出し元の情報を保持している RubyVM::AbstractSyntaxTree.of(caller_locations[0]) end p target #=> #<RubyVM::AbstractSyntaxTree::Node:VCALL@5:2-5:8> # ^^^^^^ Line 5, Column 2--8
2021/06/04 今週の気になった bugs.ruby のチケット
今週は backtrace に情報を追加する提案がありました。
[Misc #17932] 90s design (please lets move to 21st century)
- バグトラッカーを古いデザインから新しいデザインにしようぜ!っていうチケット
- github へディスカッションを移動しようぜ!みたいな内容
- この人が言うには bugs.ruby の検索が壊れているらしいが、結局一時的におかしくなっていただけで現状は問題ないっぽい
- 現状は ruby/b.r-l.o の方で議論するようにコメントされてチケット自体や Reject されている
- https://bugs.ruby-lang.org/issues/17932#note-4
- これが bugs.ruby 本体のリポジトリっぽい?
- これとは関係ないが bugs.ruby と ruby/ruby の github と各 bundled gem のリポジトリのどこで議論するのか分からん問題があるので、そのあたりをルール化してほしい気持ちは無きにしもあらず
[Feature #17930] Add column information into error backtrace
backtrace
にエラー位置情報を追加する提案- 具体的には以下のような情報を追加するイメージ
Thread::BacktraceLocation#first_lineno
Thread::BacktraceLocation#first_column
Thread::BacktraceLocation#last_lineno
Thread::BacktraceLocation#last_column
- こういう情報を追加することで以下のように『エラー箇所を明示化できる』ようにできるユースケースが提示されている
$ ruby -r ./sample/no_method_error_ext.rb err1.rb err1.rb:2:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError) data["data"].first["field"] ^^^^^^^^^
- undefined method `[]' for nil:NilClass (NoMethodError) だけだとどこでエラーになっているのかわからないのでこれは便利そう
data
がnil
なのかdata["data"].first
がnil
なのかがわからない
[Bug #17889] Enumerator::Lazy#with_index should return size
Enumerator::Lazy#with_index
の戻り値に対してsize
を呼ぶと意図しない値が返ってきたというバグ報告
p Enumerator::Lazy.new([1, 2, 3], 3){|y, v| y << v}.with_index.size # 期待する値 => 3 # 実際の値 => nil
2021/05/30 今週の気になった bugs.ruby のチケット
今週は prepend
した際の定数探索のバグ報告がありました。
[Bug #17887] Missed constant lookup after prepend
- 次のように
prepend
したときの定数の探索がおかしいというバグ報告
class A FOO = 'a' end class B < A def foo FOO end end b = B.new # スーパークラスの FOO を参照するのでこれは正しい p b.foo # => "a" module M FOO = 'm' end A.prepend M # A に prepend すると B の継承リストはこうなる p B.ancestors # => [B, M, A, Object, Kernel, BasicObject] # A よりも M が前にあるので M::FOO を参照してほしいが実際には A::FOO を参照している p b.foo # 期待する挙動 => "m" # 実際の挙動 => "a"
- ちなみに
B.include M
をすると期待する動作をする
class A FOO = 'a' end class B < A def foo FOO end end b = B.new module M FOO = 'm' end B.include M # A.prepend M と同じ継承リスト p B.ancestors # => [B, M, A, Object, Kernel, BasicObject] # これは期待する挙動になる p b.foo # => "m"
[Feature #12913] A way to configure the default maximum width of pp
pp
で表示幅を指定する時にPP.default_maxwidth
みたいなグローバル値で設定できるようにする提案
require "pp" # 現状幅を指定する場合は第三引数に幅数を渡す必要がある # NOTE: チケットで提示されていたのは pp だけど現状は動かなくなってるぽい PP.pp([1, 2, 3], $>, 1) # => [1, # 2, # 3]
- グローバル値だと意図しない箇所にまで範囲にまで影響を受けてしまうので危険だとコメントされている
- 現在は
PP.pp
とKernel#pp
でwidth:
キーワード引数を受け取るような形で提案がされている
pp([1, 2, 3], width: 1)
- ただし、この実装だと現在の挙動と非互換になる
# 現状は Hash として出力される pp([1, 2, 3], width: 1) # => [1, 2, 3] # {:width=>1}