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

[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

[Feature #17753] Add 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 って名前になっている

[Feature #17749] Const source location without name

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

  • 2x2 * 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

マージされたチケット

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

  • irbrequire のファイル名補完をする機能の追加
irb(main):001:0" require "irb/<Tab>  # <- Tab を押すとファイル名を補完してくれる
  • これは普通に便利そう

[PR irb #204] Add whereami command

  • irbprywhereami コマンドを追加する PR
    • whereami すると 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

  • irbpryls コマンドを追加する PR
    • ls すると引数オブジェクトのメソッド一覧やインスタンス一覧を出力してくれる
$ 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

[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 すると例外が発生するがソート済みになっているというバグ報告
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#[] が反映されない

  • 以下のように Arrayprepend してる #[] が呼び出されないことがある
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"
  • これは Ruby 3.0 で再現する
    • Ruby 2.7 では再現しない
    • Ruby 3.0 に上げると急にぶっ壊れるかもしれないので注意する…

[Bug #16996] Hash should avoid doing unnecessary rehash

[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

# 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>