2021/04/01 今週の気になった bugs.ruby のチケット
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。
[Bug #17757] Hash#slice does not keep compare_by_identity on the results
Hash#compare_by_identity
を使用するとレシーバの Hash をキーの一致判定をオブジェクトの同一性で判定するようにするObject#object_id
で判定するようにする
hash = { "a" => 1, b: 2 } # デフォルトでは無効 p hash.compare_by_identity? # => false # OK p hash["a"] # => 1 p hash[:b] # => 1 # Object#object_id で判定するようにする hash.compare_by_identity p hash.compare_by_identity? # => true # ハッシュのキーの "a" と 参照するときの "a" の object_id は異なるので参照できない p hash["a"] # => nil # Symbol は同じ object_id なので参照できる p hash[:b] # => 2
- この
compare_by_identity
だが特定のメソッドを経由して生成した Hash でcompare_by_identity
の性質を受け継いだりしなかったりするというバグ報告
hash = { "a" => 1, b: 2 } # Object#object_id で判定するようにする hash.compare_by_identity # こっちは compare_by_identity を保持するが p hash.except("a").compare_by_identity? # => true # こっちは compare_by_identity を保持しない p hash.slice("a").compare_by_identity? # => false
- なるほど…?
compare_by_identity
なんてものがあるの知らなかった
[Bug #17754] NoMethodError#to_s makes segmentation fault when Module#name returns non string value
- 以下のように
.name
が文字列以外を返した場合に SEGV するというバグ報告
class C def self.name 42 # これなら OK # "42" end end # C に対して NoMethodError なエラーが発生すると SEGV する C.this_method_does_not_exist
- これは Ruby 3.0 で再現して 2.7 や 2.6 では再現しなかった
- 3.1.0-dev で既に修正済み
- よく見つけるなあこんなバグ…
[Bug #17756] StringScanner#charpos makes segmentation fault when target.byteslice returns non string value
- 以下のように
StringScanner
を使用すると SEGV するというバグ報告byteslice
を上書きしてnil
を返すと落ちるっぽい?
require 'strscan' string = 'ruby' scnanner = StringScanner.new(string) pre = Module.new do def byteslice(*args) end end string.singleton_class.prepend(pre) scnanner.charpos
- これは既に対応済み
- そもそも
StringScanner
の存在を知らなかった- 指定した文字列をスキャンするためのライブラリ
- Ruby | StringScanner で字句解析 - Qiita
- Rubyの標準ライブラリStringScannerがすごい! - hythofの日記
[Feature #17753] Add Module#outer_scope
- 自分が定義されている1つ上のスコープのオブジェクトを取得する
Module#outer_scope
メソッドの提案
module A module B class C; end class D; end end end # C が定義されている箇所のスコープを返す p A::B::C.outer_scope # => B # ネストして呼び出したりとか p A::B::C.outer_scope.outer_scope # => A
- ユースケースとしては次のように同じスコープの定数を列挙したい場合に利用できる
A::B::C.outer_scope.constants # => [A::B::C, A::B::D] ObjectSpace.each_object(Class) do |k| p siblings: k.outer_scope.constants end
- 現在は『
outer_scope
って名前じゃなくてnamespace
のほうがいいんじゃない?』と言われてModule#namespace
って名前になっている- https://bugs.ruby-lang.org/issues/17753#note-2
- 流石に
Module#namespace
って名前をここで使っちゃうのはなんかもったいない気がするなあ…
[Feature #17749] Const source location without name
Module#const_source_location
を『レシーバ』に対して行う機能を追加する提案A::B.const_source_location
はA.const_source_location(:B)
と同じ意味になる- PR: https://github.com/ruby/ruby/pull/4324
- Module#const_source_location (Ruby 3.0.0 リファレンスマニュアル)
module A class B end end p A::B.const_source_location # => ["test.rb", 2] # これと同じ意味 p A.const_source_location(:B) # => ["test.rb", 2] # ユースケース # const_source_location に渡す定数名がわからない場合に利用できる ObjectSpace.each_object(Class) do |k| p k.const_source_location end
- これは普通に前からほしいなーと思ってたのでうれしい
- この機能自体は『定数』とは関係ないので
const_source_location
じゃなくてsource_location
という名前に変わっている
[Feature #17768] Proposal: Downward assignments
- 次のように変数を下の行で定義する提案
# var = 42 と同じ 42 ^^var # 中間の式も代入できる p(2 * 3 * 7) #=> 42 ^^^^^var p var #=> 6
- 実際のユースケースいろいろ
while (line = gets) != nil p line end # ↓↓↓ while gets != nil ^^^^line p line end
ary = [1, 2, 3, 4, 5] ary.each {|elem| found = elem if elem.even? } p found #=> 4 # ↓↓↓ ary = [1, 2, 3, 4, 5] ary.each {|elem| elem if elem.even? } ^^^^found p found #=> 4
class C def initialize(foo, bar) @foo = foo @bar = bar end end # ↓↓↓ class C def initialize(foo, bar) ^^^@foo ^^^@bar end
- めっちゃ便利そうじゃん…(※もちろんこれはエイプリルフールネタです
- ちゃんと patch ファイルも付いてて以下のコードは普通に動いてる
(2 * 3 * 7) ^^^^^var p var while gets != nil ^^^^line p line end ary = [1, 2, 3, 4, 5] ary.each {|elem| elem if elem.even? } ^^^^found p found #=> 4
- どうやってやってるの…
[Feature #17769] Proposal: numeric coefficient syntax
2x
を2 * x
と解釈するようにする提案
irb(main):001:0> x = 3 => 3 irb(main):002:0> 2x => 6 irb(main):003:0> def pi = Math::PI => :pi irb(main):004:0> 2pi => 6.283185307179586
2x
だけではなくて2(x + 1)
という風にもかけるらしい- これもエイプリルフールネタ
- patch 内容が比較的シンプルで面白い
マージされたチケット
defx で絞り込みしつつ dentie-file/rec を起動する
defx で絞り込みしつつ dentie-file/rec を起動する
defx で find っぽいことをしたかったので設定した。
単に :Denite file/rec -input=ファイル名
を起動しているだけ。
:Defx -auto-cd
で起動しておく必要があるので注意。
" NOTE: auto-cd を有効にしておく必要があるので注意 call defx#custom#option('_', { 'auto_cd': 1, }) function! s:start_find() let find_word = input("Find Pattern: ") call denite#start([{ "name": "file/rec", "args": []}], { "input": find_word }) endfunction autocmd FileType defx call s:defx_my_settings() function! s:defx_my_settings() abort " 起動するキーマップ nnoremap <silent><buffer> gd :<C-u>call <SID>start_find()<CR> endfunction
denite.nvim の denite-menu を使って簡単にファイルを開いたりする
dentie-menu を使うことで簡単に自分好みのショートカットを設定する source を定義することができます。
" denite-menu の設定 let s:menus = {} " Denite menu:dotfile をすると開くファイルを登録する let s:menus.dotfile = { \ 'description': 'Edit your dotfile' \ } let s:menus.dotfile.file_candidates = [ \ ['vimrc', '~/.vimrc'], \ ['gvimrc', '~/.gvimrc'], \ ['bashrc', '~/.bashrc'], \ ['bash_aliases', '~/.bash_aliases'], \ ] " Denite menu:my_denites をすると開くファイルを登録する " コマンドを呼ぶ let s:menus.my_denites = { \ 'description': 'Denite list' \ } let s:menus.my_denites.command_candidates = [ \ ['file/old', 'Denite file/old'], \ ['menu:dotfile', 'Denite menu:dotofile'], \ ['grep', 'Denite grep'], \] " 登録 call denite#custom#var('menu', 'menus', s:menus) " 呼び出し nnoremap <Space>ll :Denite menu<CR>
べんりべんり。
2021/03/25 今週の気になった bugs.ruby のチケット
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。
[PR irb #212] Complete require and require_relative
- irb で
require
のファイル名補完をする機能の追加
irb(main):001:0" require "irb/<Tab> # <- Tab を押すとファイル名を補完してくれる
- これは普通に便利そう
[PR irb #204] Add whereami command
irb
にpry
のwhereami
コマンドを追加する PRwhereami
するとbinding.irb
した付近のコードを確認することができる
$ ruby /tmp/a.rb From: /tmp/a.rb @ line 3 : 1: a = 1 2: @b = 2 => 3: binding.irb irb(main)[01:0]> a => 1 irb(main)[02:0]> @b => 2 irb(main)[03:0]> whereami From: /tmp/a.rb @ line 3 : 1: a = 1 2: @b = 2 => 3: binding.irb => nil
- pry の
whereami
コマンド知らなかった - これはめっちゃ便利そう…
[PR irb #203] Implement pry-like ls command
irb
にpry
のls
コマンドを追加する PRls
すると引数オブジェクトのメソッド一覧やインスタンス一覧を出力してくれる
$ irb irb(main):001:0> require "erb" => true irb(main):002:0> ls ERB.new('test') ERB#methods: def_class def_method def_module encoding filename filename= lineno lineno= location= make_compiler result result_with_hash run set_eoutvar src instance variables: @_init @encoding @filename @frozen_string @lineno @src => nil
- これも知らなかった
- めっちゃ便利そう…
[Feature #17411] Allow expressions in pattern matching
- 以下のように
^
を使ってパターンマッチに式をかけるようにする提案
user = { name: "homu", age: 14 } case user # 式を書く場合は ^() を使う in { age: ^(7 + 7) } end
- これが実装されてマージされた :tada:
- 早くパターンマッチ使いたい…
[Bug #17738] Ruby can still freeze ENV
ENV
は普通はfreeze
できないががんばればfreeze
できるという報告
# これは error ENV.freeze # こうすると freeze できる Kernel.instance_method(:freeze).bind(ENV).call() p ENV.frozen? #=> true
- Ruby むずかしい…
[Bug #17735] Hash#transform_keys!
drops non evaluated keys
- 以下のように
Hash#transform_valies!
のブロック内でraise
するとレシーバは変わらない
# transform_values! はレシーバの要素を保持する hash = {a: 1, b: 2, c: 3} a = hash.transform_values! { raise } rescue p hash #=> {:a=>1, :b=>2, :c=>3}
- しかし
Hash#transform_keys!
の場合はレシーバが空になる
# transform_keys! はレシーバの要素が空になる hash = {a: 1, b: 2, c: 3} hash.transform_keys!(){ raise } rescue p hash #=> {}
[Bug #17736] Destructive methods inconsistently handle receiver frozen state
in given block
Array#select!
のブロック内でレシーバをfreeze
するとレシーバが空の配列になる
array = [1, 2, 3, 42] array.select! do array.freeze false end p array #=> []
Array#uniq!
の場合は例外になりレシーバはそのままになる
array = [1, 2, 3, 42, 2, 3] begin array.uniq! do |item| array.freeze item end rescue => err p err #=> #<FrozenError: can't modify frozen Array: [1, 2, 3, 42, 2, 3]> end p array #=> [1, 2, 3, 42, 2, 3]
Array#select!
の方はバグということで修正された
array = [1, 2, 3, 42] # Ruby 3.1.0-dev だとエラー # error: `select!': can't modify frozen Array: [1, 2, 3, 42] (FrozenError) array.select! do array.freeze false end
[Bug #17739] Array#sort! changes the order even if the receiver raises FrozenError in given block
Array#sort!
のブロック内でレシーバをfreeze
すると例外が発生するがソート済みになっているというバグ報告- [Bug #17736] と同じようなバグ
array = [1, 2, 3, 4, 5] begin array.sort! do |a, b| array.freeze if a == 3 1 end rescue => err # 例外が発生する p err #=> #<FrozenError: can't modify frozen Array: [5, 4, 3, 2, 1]> end # 例外が発生してもソート済みになっている p array #=> [5, 4, 3, 2, 1]
- ちなみに
break
した場合はそこまでのソートになっている
array = [1, 2, 3, 4, 5] array.sort! do |a, b| break if a == 3 1 end # 途中までソートされた状態 p array #=> [3, 4, 2, 1, 5]
- これはまだ未修正
[Feature #17743] Show argument types in backtrace
- バックトレースに引数情報も追加する提案
- 例えば以下のようなコードを実行すると
def say_hi(person) puts message(person) end def message(person) "hi: #{person.name}" end say_hi(nil)
- 以下のようなバックトレースが出力される
/tmp/vTaZxJg/70:6:in `message': undefined method `name' for nil:NilClass (NoMethodError) from hi.rb:2:in `say_hi' from hi.rb:9:in `<main>'
- これを以下のようにする提案
hi.rb:6:in `message': undefined method `name' for nil:NilClass (NoMethodError) from hi.rb:2:in `say_hi' called with NilClass from hi.rb:9:in `<main>' called with NilClass
- どのような表記にするのか(クラスだけ?値は必要がない?複数の引数の場合は?)やパフォーマンス的な懸念点がないか議論されている
[Feature #17744] Accumulate Enumerable#tally
results
- 以下のように
Enumerable#tally
に引数を渡してそれに結果を集計していくような提案- 元々は今月の開発者会議で提案されていたっぽい
h = {} [:a,:b,:c].tally(h) [:a,:b,:d].tally(h) # 2個の tally の結果が h に蓄積される p h #=> {:a=>2, :b=>2, :c=>1, :d=>1}
- 便利な気がするけど引数が変更されてしまうのはなんかちょっと気持ち悪い…
2021/03/18 今週の気になった bugs.ruby のチケット
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。
[PR irb #202] process multi-line pastes as a single entity
- 現行の
irb
だと以下のようなコードをペーストするとエラーになる
class A def a; self; end def b; true; end end a = A.new a .a .b
irb(main):001:1* class A irb(main):002:1* def a; self; end irb(main):003:1* def b; true; end irb(main):004:0> end = > :b irb(main):005:0> irb(main):006:0> a = A.new = > #<A:0x00005588503197b8> irb(main):007:0> irb(main):008:0> a = > #<A:0x00005588503197b8> irb(main):009:0> .a /home/worker/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.4/lib/irb/workspace.rb:116:in `eval': (irb):9: syntax error, unexpected '.' (SyntaxError) from /home/worker/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.4/exe/irb:11:in `<top (required)>' from /home/worker/.rbenv/versions/3.0.0/bin/irb:23:in `load' from /home/worker/.rbenv/versions/3.0.0/bin/irb:23:in `<main>' irb(main):010:0> .b
- これはペーストしたタイミングで Ruby のコードが逐次的に評価され
.a
を呼び出した時にエラーになってしまっているため- 先に
a
のコードが評価され
- 先に
- これを回避するためにペーストして『最後に』コードを評価するようにする PR
- 個人的には落とし所としてはいいとは思う
[Bug #17571] prependしたArray#[] が反映されない
- 以下のように
Array
にprepend
してる#[]
が呼び出されないことがある
module TestMod def [](*) :called end end Array.prepend TestMod # これは Array#[] が呼ばれる p [1, 2, 3][1] # => 2 # これは TestMod#[] が呼ばれる p [1, 2, 3][] # => :called # Method オブジェクトは TestMod を指している p [1, 2, 3].method(:[]) # => #<Method: Array(TestMod)#[](*) /tmp/vud3mdg/27:2>
- これはおそらく
Array#[]
を事前にメソッドキャッシュしておりそちらを優先して呼び出しているのが原因ぽい?- なので
prepend
しているメソッドは呼ばれなくなっている - 引数がない場合は
Array#[]
とシグネチャが異なるのでキャッシュされたメソッドではなくてprepend
されたメソッドを呼び出しているので意図する挙動になっているみたい?
- なので
- これは Ruby 2.7 では問題なくて Ruby 3.0 から問題になっている
- Ruby 3.0 に上げるタイミングでなにか壊れるかもしれないので注意したい
- 原因っぽいコミット(と PR)
[Bug #17725] Prepend breaks ability to override optimized methods
- 以下のように
String.prepend
するとString#+
が上書きされたりされなかったりする
# これは上書きされる class String def + other 'blah blah' end end p 'a' + 'b' # => "blah blah"
# これは上書きされない String.prepend(Module.new) class String def + other 'blah blah' end end p 'a' + 'b' # => "ab"
[Bug #16996] Hash should avoid doing unnecessary rehash
Hash#dup
よりもHash#merge
の方が高速だというチケット- 3.5倍早いと書かれている
- これは
Hash#dup
は内部で再ハッシュを行っているがHash#merge
は行っていないのでその差になる - Ruby なにもわからない
Hash#replace/dup/initialize_copy
で再ハッシュを行わないようにする PR- 開発者会議でも承認されたっぽいのでマージされそう
- ちなみに
hash.select
よりもhash.merge.select!
のほうが高速らしいHash#select/reject
で再ハッシュしないようにする PR- https://github.com/ruby/ruby/pull/4273
[Bug #17719] Irregular evaluation order in hash literals
Hash
リテラルでキーが重複している場合に以下のような評価順になる
# 1個目と2個目の foo の要素が先に評価される $ ruby -e '{foo:p(1), bar:p(2), foo:p(3)}' -e:1: warning: key :foo is duplicated and overwritten on line 1 1 3 2
- これはバグのようなので修正するチケット
- 以下のような問題もあるしもうエラーにしちゃっていいんじゃないかなあ…。
Ruby の Hash リテラルでキーが重複している場合の奇妙な動作
最近こういう動作を見つけたので覚書。
Hash リテラルでキーが重複しているとどうなるのか
Hash リテラルで Hash を定義した場合、キーの順番は『記述した順番』になります。
# ここで定義した順番になる hash = { age: 14, name: "homu" } pp hash # => {:age=>14, :name=>"homu"} hash = { name: "homu", age: 14 } pp hash # => {:name=>"homu", :age=>14}
では、この Hash リテラルでキーが重複しているとどうなるのかというと次のようになります。
# age が重複している場合 hash = { id: 1, age: 14, name: "homu", age: 20, age: 10 } # キーの順番は age が先にくるが、値は最後に定義した値になる pp hash # => {:id=>1, :age=>10, :name=>"homu"}
こんな感じで
- キーの位置は『最初に定義された位置』
- キーの値は『最後に定義された値』
という挙動になります。
キーの位置と値が異なるのでちょっと混乱しますね。
このような書き方をすると警告もでますし、基本的に Hash リテラルではキーを重複して書かないようにしましょう。
# warning: key :age is duplicated and overwritten on line 2 hash = { id: 1, age: 14, name: "homu", age: 20, age: 10 }
関連
2021/03/11 今週の気になった bugs.ruby のチケット
内容は適当です。
今週と言っても今週みかけたチケットなだけでチケット自体は昔からあるやつもあります。
あくまでも『わたしが気になったチケット』で全ての bugs.ruby のチケットを載せているわけではありません。
[Bug #16908] Strange behaviour of Hash#shift when used with default_proc
.
- 以下のように
default_proc
が設定されている状態でHash#shift
を呼ぶといと意図しない値が返ってくるよっていうチケットHash
が空の時にshift
を呼ぶとdefault_proc
の値が返ってくる
hash = Hash.new{|k,v| k[v] = 0} hash.shift # => 0 hash.shift # => [nil, 0]
- 意図としては両方共
[nil, 0]
が返ってきてほしい - もうちょっと詳細に説明するとこんな感じ
hash = Hash.new{|k,v| k[v] = 0} # 空 p hash # => {} # ここは default_proc 値を返す # ここが意図していないというチケット p hash.shift # => 0 # hash,shift 後は中身が入ってる状態になる p hash # => {nil=>0} # ので、これは [nil, 0] を返す p hash.shift # => [nil, 0]
- この挙動は確かに奇妙
- チケットだと
nil
を返すほうがいい、みたいな意見もある
[Feature #17674] Proposal: Method#source_location
or Method#owner
for refined methods
- https://bugs.ruby-lang.org/issues/15504#note-17 からの派生
- https://github.com/AlexWayfer/gorilla_patch/blob/master/lib/gorilla_patch/cover.rb#L8 こんな実装を書いてある場合に
range.method(:cover?)
でsource_location
が取れなくてそれを対応させてほしいっぽい?- Ruby 2.6 だと
refine + using
しててもrange.method(:cover?) # => nil
になってしまう
- Ruby 2.6 だと
- これは Ruby 2.7 だと既に対応済み
# test.rb module Cover refine Range do def cover?(value) return super unless value.is_a?(Range) super(value.first) && super(value.last) end end end using Cover pp (1..10).method(:cover?).source_location # Ruby 2.6 => nil # Ruby 2.7 => ["/test.rb", 4] pp (1..10).method(:cover?).owner # Ruby 2.6 => Range # Ruby 2.7 => #<refinement:Range@Cover>