2021/09/16 今回の気になった bugs.ruby のチケット

今週は { a: a, b: b }{ a:, b: } とかけるショートハンドが入りました。

[Feature #18168] Add ActiveSupport deep_transform_values to Ruby

require "active_support/all"

hash = { person: { name: 'Rob', age: '28' } }

# ネストした Hash すべてに value.to_s.upcase を適用させる
pp hash.deep_transform_values { |value| value.to_s.upcase }
# => {:person=>{:name=>"ROB", :age=>"28"}}
  • 再帰的に参照している場合は SystemStackError になっちゃうみたいですね
require "active_support/all"

hash = { a: 1 }
hash[:hash] = hash

# 再帰的に参照してる場合はエラー
# error: stack level too deep (SystemStackError)
pp hash.deep_transform_values{ |value| value.to_s.upcase }
  • チケットの内容が虚無なのでこのままだと流れていきそうな雰囲気

[Bug #18160] IndexError raised from MatchData#{offset,begin,end} does not keep the encoding of the argument

pp RUBY_VERSION  # => "3.0.2"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: \xE3\x81\x82"
  • これは最新版では修正されて正しい意図する文字コードで出力される
pp RUBY_VERSION  # => "3.1.0"

m = /.*/.match("foo")
m.offset("\u{3042}") rescue p $!.message
# => "undefined group name reference: あ"
  • 便利

[Feature #14579] Hash value omission

  • { x:, y: }{ x: x, y: y } のショートハンドにする提案
  • これ以外にも似たような提案は無限にされていたけど matz を説得できずに長年入っていなかった
  • 先日の RubyKaigi の感想戦でこれに関する議論がされて、matz を説得して無事に accepts された :tada:
  • これは以下のように x:x: x に展開するようなシンタックスシュガーになる
name = "homu"
age = 14

# { name: name, age: age } のシンタックスシュガー
{ name:, age: }
# => {:name=>"homu", :age=>14}

# 一部だけ値を割り当てることもできる
{ name:, age: 16 }
# => {:name=>"homu", :age=>16}


def tokyo
  "東京"
end

# メソッド呼び出しも可能
{ tokyo: }
# => {:tokyo=>"東京"}


def user(name:, age:)
  { name:, age: }
end

# user(name: name, age: age) になる
user(name:, age:)


# 現時点では定数も展開できる
X = 42
{ X: }
# => {:X=>42}
  • 現状は { X: } みたいな定数も展開されるがもしかしたらリリース時点でまた仕様が変わっている可能性があるので注意
  • ちなみにこれを利用すると if 変数に対して高速にアクセスする事ができる
def foo(if:)
  # if はキーワードなので変数にアクセスする時は Binding#local_variable_get を使う必要があった
  if_ = binding.local_variable_get(:if)

  # このショートハンドを使うとこうかけるようになる
  if_ = {if:}[:if]
end

[Feature #17355] Using same set of names in or-patterns (pattern matching with Foo(x) | Bar(x))

  • 以下のようにパターンマッチで束縛名が重複していると今はエラーになる
case [1, 2]
in [1, a] | [a, 3] then a
end
  • これをエラーではなくて各パターンごとで束縛できるようにする提案
  • これがあると例えば以下のようなコードが
def user_email(user)
  case user
  in User(email:) then email
  in Admin(email:) then email
  in Moderator(email:) then email
  end
end
  • 以下のように1つのパターンで定義する事ができる
def user_email(user)
  case user
  in User(email:) | Admin(email:) | Moderator(email:) then email
  end
end
  • 今はまだ実装中?らしい
  • これは普通にほしいなあ

[Misc #18150] Proposal: Deprecate leading zero syntax to declare octals, since it's extremely confusing (and Python 3 removed it too)

  • 8進数リテラルについて議論するチケット
  • 現状の Ruby0 が先頭に付いていると8進数として解釈される
    • 012 # => 10
  • また 0o が付いている場合も8進数として解釈される
    • 0o12 # => 10
  • なのでうっかり次のようなコードを書いてしまうとこれはエラーになってしまう
# "2021-09-01" のつもりで 09 と書いてしまった
# これはエラーになる
START_DATE = Date.new(2021, 09, 01)
# error: Invalid octal digit
  • このようなケースがあるので 0 を使った8進数リテラルRuby 3.x では非推奨にし、Ruby 4.0 では 0011 => # 11 にしよう、という提案
  • ちなみに先頭 0 が8進数リテラルになっている言語は JavaScript Go Java C がある
  • 逆にそうでない言語は RustElixir とのこと
  • また Python3+ はエラーになる
  • 個人的には 8進数リテラルを使ったことがないので気にはならないけど、既存のコードを非互換にしてまで入れる必要があるのかと言われると難しいところ

[Feature #18146] Add delete_prefix and delete_suffix to Pathname

  • Pathname#delete_prefixdelete_suffix を追加する提案
  • 元々は RuboCop が誤検知していたのが起因らしい
  • 誤検知を本体に実装しようとするアイディアがすごい(褒めてる)

[ruby/un] Add colorize command

  • 普通に便利そう

[Feature #16182] Should expr in a, b, c be allowed or not?

  • 元々は Ruby 2.7 時点での expr in [a, b, c] という1行 in の [] を省略できるようにするかどうかを議論するチケット
  • Ruby 2.7 (3.0) では []{} は省略できないようになっていた
# これらはシンタックスエラー
{ name: "homu" } in name:
[1, 2] in a, b
# OK
{ name: "homu" } in name:
[1, 2] in a, b
  • ちなみに右代入も許容されている
# OK
{ name: "homu" } => name:
[1, 2] => a, b
  • また、メソッドの引数に渡す場合は明示的に () を付ける必要があるので注意する
# これはエラー
puts([1] in String)

# () をつけると OK
puts(([1] in String))

Ruby 3.1 で { a: a } が { a: } とかけるようになるらしい

以前から山のように提案があった Hash のショートハンドが RubyKaigi の感想戦で matz を説得して入ったらしい。

これによって Ruby 3.1 で Hash が以下のように書くことができます。

name = "homu"
age = 14

# { name: name, age: age } のシンタックスシュガー
{ name:, age: }
# => {:name=>"homu", :age=>14}

# 一部だけ値を割り当てることもできる
{ name:, age: 16 }
# => {:name=>"homu", :age=>16}


def tokyo
  "東京"
end

# メソッド呼び出しも可能
{ tokyo: }
# => {:tokyo=>"東京"}


def user(name:, age:)
  { name:, age: }
end

# user(name: name, age: age) になる
user(name:, age:)

めっちゃ便利ですね!!!!
このショートハンドは { a: } -> { a: a } というよりかは a: -> a: a になる、と認識するとよさそうです(なので [a:] -> [a: a] となる。
matz を説得している時にちょうど離席していたのでどのような経緯で matz を説得したのかがすごく気になる…。
これは既に本体にコミットされているので rbenv install 3.1.0-dev などで最新の Ruby を落としてくるとすぐに使えます。
今すぐ早く使いたい。

おまけ

感想戦binding.local_variable_get(:if) の変わりに {if:}[:if] が使えるようになると言及されててワロタ

def foo(if:)
  # if はキーワードなので変数にアクセスする時は Binding#local_variable_get を使う必要があった
  if_ = binding.local_variable_get(:if)

  # このショートハンドを使うとこうかけるようになる
  if_ = {if:}[:if]
end

便利そう。

2021/09/03 今週の気になった bugs.ruby のチケット

今週は Enumerable#take_while_afterEnumerable#detect_only といったメソッドの提案がありました

[Feature #18136] take_while_after

tokens = [
  {text: 'Ruby', type: :word},
  {text: 'is', type: :word},
  {text: 'cool', type: :word},
  {text: '.', type: :punctuation, ends_sentence: true},
  {text: 'Rust', type: :word},
  # ...
]

# _1[:ends_sentence] が真になるまでの要素を返す
sentence = tokens.take_while_after { _1[:ends_sentence] }
pp sentence
# => [{:text=>"Ruby", :type=>:word},
#     {:text=>"is", :type=>:word},
#     {:text=>"cool", :type=>:word},
#     {:text=>".", :type=>:punctuation, :ends_sentence=>true}]

# これは slice_after.first と同じ挙動
sentence = tokens.slice_after { _1[:ends_sentence] }.first
pp sentence
# => [{:text=>"Ruby", :type=>:word},
#     {:text=>"is", :type=>:word},
#     {:text=>"cool", :type=>:word},
#     {:text=>".", :type=>:punctuation, :ends_sentence=>true}]

[Feature #18135] Introduce Enumerable#detect_only

  • #detect で要素が見つからなかった場合に例外を返す Enumerable#detect_only メソッドの提案ぽい?
  • #detect で要素が1個だけ見つかった場合は成功し、それ以外は例外を返す Enumerable#detect_only メソッドの提案
  • 次のようなコードを
matches = array.select { |elem| some_method(elem) }
raise if matches.size != 1
match = matches.first
  • 以下のようにかけるようにする提案らしい
match = array.detect_only { |elem| some_method(elem) }

[Bug #18126] Process termination three seconds after thread termination dumps core

  • 次のようなコードを実行するとランダムで segv することがあるらしい
3000.times{Thread.new{}}
sleep 2.99
$ ruby test.rb
[BUG] Segmentation fault at 0x0000000000000440
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-linux]

$
  • これは
    • Ruby のスレッドが終了し、3分以内に新しい Ruby のスレッドが作成されると、その内部の pthread が別のRuby のスレッドによって再利用される
    • しかし、pthread が3分待機している間に Ruby のプロセスプロセスが RubyVM と GC が破棄される
    • その後、xfreeは使用できなくなり、セグメンテーション違反が発生する
  • らしい

[Misc #18125] A strange behavior when same name variable/method coexist issue.

  • 次のように同名のメソッドと変数を定義した時に奇妙な動作になるけどこれは期待する動作?という質問
def deploy_to
  "deploy_to"
end

deploy_to = "#{deploy_to} new place"
#              ^ これがメソッドではなくて変数を参照している

p defined? deploy_to # => local_varible
p deploy_to  # => " new place"
  • これは期待する動作でこの場合は変数が定義され値が割り当てられる前のローカル変数 deploy_to を参照する
  • メソッドを参照したい場合は () を付けてメソッド呼び出しだと明示化する必要がある
def deploy_to
  "deploy_to"
end

deploy_to = "#{deploy_to()} new place"
#              ^ これはメソッド呼び出しになる
p deploy_to  # => "deploy_to new place"

RubyKaigi Takeout 2021 で Ruby のマクロについて話します

先日 RubyKaigi Takeout 2021 のスピーカーとスケジュールが発表されました。
わたしも 3日目の朝一に Use Macro all the time ~ マクロを使いまくろ ~というタイトルで登壇させていただきます。
内容はタイトルにも書かれている通りマクロの話をします。
マクロと言ってもいわゆる C言語のようなプリプロセスマクロではなくて『AST レベルで Ruby の構文を別の構文へと変換すること』を Ruby のマクロと定義し、Ruby でどのようにマクロを表現するのか、みたいな話をする予定です。
まあもっとさっくりう言うと『Ruby でマクロを実装してみたらこうなりました』みたいな感じですかねえ。
例えばマクロを使用することで、

hoge.foo.bar

のようなコードを

hoge&.foo&.bar

のようにぼっち演算子呼び出しとして変換する事ができたり、

CONST_HOGE = [1, 2, 3]

というような定数定義しているコードを

CONST_HOGE = [1, 2, 3].freeze

のように暗黙的に freeze したり、

![a, b, c]

というようなコードを

{ a: a, b: b, c: c }

みたいな Hash に展開するようなことを実現することができます。
Ruby のマクロは以下のような過程を経て Ruby のコードから別の Ruby のコードへと変換しています。

開発中のコードをちょっとだけお見せすると Ruby のマクロを使うことによって以下のようなことを実現することができます。

# ※開発中のコードなので当日までに変わるかもね!
using Macro::Refine::Source


# 特定の構文を変換するマクロを定義する
module MyMacro
  using Macro::Macroable

  # cat! というメソッド呼び出しを任意のコードに変換する
  def cat!(num = ast { 1 })
    ast { "にゃーん" * $num }
  end
  macro_function :cat!

  # 範囲リテラルを freeze するコードに変換する
  def freezing(node, parent)
    ast { $node.freeze }
  end
  macro_node :DOT2, :freezing
  macro_node :DOT3, :freezing

  # 代入式を freeze するコードに変換する
  def assign_freezing(node, name:, value:)
    ast { $name = $value.freeze }
  end
  macro_pattern pat { $name = $value }, :assign_freezing
end


# 変換したい Ruby のコードを Proc で定義
body = proc {
  use_macro! MyMacro

  puts cat!
  puts cat!(3)

  value = [2, 3, 5, 7, 11, 13, 17].grep((0..10))

  HOGE = "にゃーん" + "にゃーん"
}

# マクロを適用させた AST に変換する
result = Macro.compile_of(body)

# 変換させた AST から Ruby のソースコードを取得する
pp result.source

# 以下のようなコードに変換される
__END__

# cat!(n) が "にゃーん" * n に変換される
puts "にゃーん" * 1
puts "にゃーん" * 3

# (0..10) が (0..10).freeze に変換される
# value = hoge が value = hoge.freeze に変換される
value = [2, 3, 5, 7, 11, 13, 17].grep((0..10).freeze).freeze

HOGE = ("にゃーん" + "にゃーん").freeze

上記のような書き方で Ruby のコードを AST レベルで別のコードへと変換するような実装を書いています。
ちなみに実装はすべて Ruby で行っているので MRI 側で特に何か機能を追加したりとかはしていません。
この登壇でメタプロの向こう側ってやつを見せてやりますよ(誇張。
と、言うことでこういう話が好きな方は当日までお楽しみに! ちなみに去年を除くとこれが RubyKaigi 初参加になります。
RubyKaigi 初参加で初登壇の実績を解除したぜ…。

2021/08/19 今週の気になった bugs.ruby のチケット

今週は右代入で特定のケースでシンタックスエラーになるというバグ報告がありました。

[Bug #18084] JSON.dump can crash VM.

  • 次のように再帰的な HashJSON.dump に渡すと VM がクラッシュするバグ報告
    • どうやら Linux 系でのみ発生しているぽい?
require 'json'

x = {}
# 自身に自身を割り当てる
x[:x] = x

# machine stack overflow in critical region (fatal)
p JSON.dump(x)
  • これは Ruby 2.7 から再現しており、Ruby 2.7 以前は SystemStackError が発生する
require 'json'

x = {}
x[:x] = x

# Ruby 2.6 の場合
# error: stack level too deep (SystemStackError)
p JSON.dump(x)
  • これが期待する動作なのかどうか、という旨がチケットに書かれていたが最新版では SystemStackError になるように修正された

[Bug #18080] Syntax error on one-line pattern matching

  • 次のようにカッコで囲まれていないパラメータを持つメソッドの戻り値を右代入で使用するとシンタックスエラーになる、というバグ報告
# パラメータがなかったり、カッコが付いている場合は OK
p do
end => a
p a #=> nil

p(1) do
end => a
p a #=> 1

# カッコがないパラメータがある場合はシンタックスエラーになる
p 1 do
end => a
#=>
# syntax error, unexpected =>, expecting end-of-input
# end => a
#    ^~

# これは1行 in でも同様にシンタックスエラーになる
p 1 do
end in a
#=>
# syntax error, unexpected `in', expecting end-of-input
# end in a
#     ^~
  • このエラーは意図的ではないが修正するのは難しいらしい

2021/08/13 今週の気になった bugs.ruby のチケット

今週は instance_execMethod#to_proc を渡すと意図しない例外が返ってくるというバグ報告や Ruby 3.0.2 で Hash#transform_keys! を呼び出すとメモリリークをするという報告がありました。

[Feature #18070] attr should be removed

  • 1.9 から非推奨だった attr を消すチケット
  • attr は以下のようなメソッド
class User
  # getter のみ定義される
  attr :id

  # true を渡すと setter も定義される
  attr :name, true
end
  • 非推奨なのは第二引数の true / false を渡すことなので attr 自体は特に問題なさそう?

[Feature #1806] instance_exec is just ignored when the block is originally a method

  • Method#to_proc の結果は元のコンテキスト情報を持っているので次のようなコードは失敗する
f = -> (x) { a + x }

class A
  def a
    1
  end
end

# これは f 内の a は A#a を参照する
A.new.instance_exec(1, &f) # => 2


class B
  def b(x)
    a + x
  end
end

# B#method の場合の場合は a が B#a を参照する
proc = B.new.method(:b).to_proc
# なのでエラーになる
A.new.instance_exec(1, &proc)
# => undefined local variable or method `a' for #<B:0x00007fdaf30480a0> (NameError)
  • このチケットの提案自体は NameError になるのではなくて ArgumentError になるべきでは?という内容
  • これ自体は期待する挙動らしい
  • なぜなら Method#to_proc の実装は以下のようになっており結果的に Method#call を呼んでいるから
class Method
  def to_proc
    method = self
    ->(*args, **kwargs, &block) do
      # ここで `B#b` が呼ばれるため結果的に NameError になる
      method.call(*args, **kwargs, &block)
    end
  end
end

[Bug #18065] 3.0.2 - possible memory leak in Hash#transform_keys!

  • 次のようなコードを Ruby 3.0.2 で実行するとメモリリークしてメモリを大量に消費するらしい
    • これは Ruby 3.0.2 で再現して Ruby 3.0.1 では問題ない
h = { value1: 1, value2: 2 }
loop { h.transform_keys!(&:to_s) }
  • この問題は既に最新版で修正済み

2021/08/05 今週の気になった bugs.ruby のチケット

今週はネストしたループで caller_locations を呼び出すと segv するバグ報告などがありました。

[Feature #18057] Introduce Array#mean

  • 配列の平均値を求める Array#mean メソッドを追加する提案
    • 元々は Array#average という名前だったが #mean という名前に変わった
array = [1, 2, 3]
array.mean # 2

# 要素の値を変更しながら適用
array = [1.5, 2.2, 3.1]
array.mean(&:round) # 2.3333333333333335

array = [-3, -2, -1]
array.mean { |e| e.abs } # 2

[Bug #18053] Crashes and infinite loops when generating partial backtraces in Ruby 3.0+

  • 以下のコードを実行すると segv するというバグ報告
    • Ruby 3.0 以降で再現する
def foo
  caller_locations(2, 1).inspect # this will segv
  # caller_locations(2, 1)[0].path # this will infinite loop
end

1.times.map { 1.times.map { foo } }
  • Ruby 3.0 の最適化のバグっぽい?
  • 修正 PR は既に出てる
  • Ruby 3.0 に関しては最適化を元に戻すのが最善らしい
    • 実装が複雑になっているので今回のバグを対応する場合は更に実装が複雑になるとのこと
    • ただし、最適化を維持したい人がいれば別途対応するとのこと
    • https://bugs.ruby-lang.org/issues/18053#note-1

[Bug #18052] Find のignore_error オプションが、文字化けファイル遭遇時の例外に対応していない (Windows)

  • Find.find(".", ignore_error: true) のように ignore_error: true を指定しても文字化けしたファイルがあると例外が発生するらしい
# coding:cp932
p __ENCODING__

# cp932 では表現できないファイル名を作る(例はハングル文字)。  
open("testfile-\uD7A3 .jpg", "w")

require "find"
Find.find(".", ignore_error: true ) {|f|
    p f
}
# => #<Encoding:Windows-31J>
#    "."
#    "./testfile-???R?s?[.jpg"
#    Traceback (most recent call last):
#            25: from d:/opt/ruby/bin/irb:23:in `<main>'
#            24: from d:/opt/ruby/bin/irb:23:in `load'
#            23: from d:/opt/ruby/lib/ruby/gems/2.6.0/gems/irb-1.3.2/exe/irb:11:in `<top (required)>'
#            6: from (irb):10:in `<main>'
#            5: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:43:in `find'
#            4: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:43:in `each'
#            3: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:48:in `block in find'
#            2: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:48:in `catch'
#            1: from d:/opt/ruby/lib/ruby/2.6.0/find.rb:51:in `block (2 levels) in find'
#    d:/opt/ruby/lib/ruby/2.6.0/find.rb:51:in `lstat': Invalid argument @ rb_file_s_lstat - ./testfile-?[.jpg (Errno::EINVAL)