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 が立ってるけど特に進展がなさそう
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