2022/05/20 今回の気になった bugs.ruby のチケット

今週はパターンマッチの #deconstruct を拡張する提案がありました。

[Feature #18788] Support passing Regexp options as String to Regexp.new

  • Regexp.new の第二引数にオプションを渡すことができる
Regexp.new('foo', Regexp::IGNORECASE | Regexp::MULTILINE | Regexp::EXTENDED) # => /foo/imx
  • これを文字列を渡せるようにする提案
Regexp.new('foo', 'i')   # => /foo/i
Regexp.new('foo', :i)    # => /foo/i

Regexp.new('foo', 'imx') # => /foo/imx
Regexp.new('foo', :imx)  # => /foo/imx
Regexp.new('foo', Regexp::IGNORECASE)   # => /foo/i

# これも i になる
Regexp.new('foo', "hoge")   # => /foo/i

# i を文字列で渡せるように見えるが
Regexp.new('foo', "i")   # => /foo/i

# これも i になる
Regexp.new('foo', "m")   # => /foo/i

[Feature #14602] Version of dig that raises error if a key is not present

  • 以下のように要素が見つからなかった場合に例外が発生する #dig! を追加する提案
hash = {
    :name => {
        :first => "Ariel",
        :last => "Caplan"
    }
}

hash.dig!(:name, :first) # => Ariel
hash.dig!(:name, :middle) # raises KeyError (key not found: :middle)
hash.dig!(:name, :first, :foo) # raises TypeError (String does not have #dig! method)
  • #[] でも似たような事は実現できるがより自明にしたいのが目的
hash = {
    :name => {
        :first => "Ariel",
        :last => "Caplan"
    }
}

hash[:name][:first] # => Ariel
hash[:name][:middle] # => nil
hash[:name][:first][:foo] # => `[]': no implicit conversion of Symbol into Integer (TypeError)

[Feature #18774] Add Queue#pop(timeout:)

[Feature #18773] deconstruct to receive a range

  • #deconstruct_keys を定義する事でパターンマッチで { a:, b: } パターンを任意のオブジェクトで使うことができる
    • 引数に Hash のキーを受け取る事ができる
class Time
  # Hash パターンのキーを受け取る
  # in { year:, day: } なら [:year, :day]
  def deconstruct_keys(keys)
    # キーを元にしてパターンマッチに必要な Hash を返す
    keys.to_h { [_1, send(_1)] }
  end
end

time = Time.new(2020, 1, 1)
pp time

# time.year と time.day を受け取る事ができる
case time
in { year:, day: }
  pp year
  pp day
end
  • 同様に [a, b] の場合は #deconstruct で拡張できる
  • この引数に [] の個数を Range で受け取る提案
class DeconstructWithRange
  def initialize(values)
    @values = values
  end

  # range で in の配列の個数を 個数..個数 で受け取る
  # * がふくまれている場合は 個数..無限 になる
  def deconstruct(range)
    range.cover?(@values.length) ? @values : []
  end
end

case DeconstructWithRange.new([1, 2])
# deconstruct(2..2) を呼び出す
in ["hoge", "foo"]
  true
# deconstruct(3..3) を呼び出す
in ["hoge", "foo", "bar"]
  true
# deconstruct(2..) を呼び出す
in ["hoge", "foo", "bar", *]
  true
else
  true
end
class ActiveRecord::Relation
  def deconstruct(range)
    # 配列の個数が一致している時のみ record を読み込んでくる
    (loaded? || range.cover?(count)) ? records : nil
  end
end

case Person.all
in []
  "No records"
# Person.all のレコード数が1個の時のみレコードを読み込んで処理する
in [person]
  "Only #{person.name}"
else
  "Multiple people"
end
  • あれば便利そうな気がするけどわざわざ Range で受け取らなくてもよい気がするなあ
    • 個数 + * があるかどうか、の2つの情報を受け取るほうが意図は伝わりやすそう?
class ActiveRecord::Relation
  # 個数と * があるかどうかを受け取る
  def deconstruct(count, rest:)
    # 配列の個数が一致している時のみ record を読み込んでくる
    (loaded? || self.count == count) ? records : nil
  end
end