💭

[Feature #21358] #digにProcを渡せるようにして、より複雑な条件で値を抽出できるようにする提案

に公開

[Feature #21358] Advanced filtering support for #dig

  • #digProc を渡せるようにして、より複雑な条件で値を抽出できるようにする提案
  • 例えば次のようなデータから battertype: "Chocolate" に対して id: の値を抽出したい場合
item = {
  id: '0001',
  batters: {
    batter: [
      { id: '1001', type: 'Regular' },
      { id: '1002', type: 'Chocolate' },
      { id: '1003', type: 'Blueberry' },
      { id: '1004', type: 'Devils Food' }
    ]
  }
}
  • 以下のように記述することで取得することができる
item.dig(:batters, :batter)&.find { it[:type] == 'Chocolate' }&.[](:id)
# => "1002"
  • これを batter の要素を抽出する際に以下のように Proc オブジェクトを渡してより複雑な条件で対象の要素を抽出できるようにする、というのが今回の提案になる
# batter の中身を抽出するときに -> { it[:type] == 'Chocolate' } を元に探す
item.dig(:batters, :batter, -> { it[:type] == 'Chocolate' }, :id)
# => "1002"
  • 実装イメージは以下のような感じ
class Array
  alias_method :original_dig, :dig

  def dig(key, *identifiers)
    case key
    when Proc
      val = find(&key)

      identifiers.any? ? val&.dig(*identifiers) : val
    else
      original_dig(key, *identifiers)
    end
  end
end

item = {
  id: '0001',
  batters: {
    batter: [
      { id: '1001', type: 'Regular' },
      { id: '1002', type: 'Chocolate' },
      { id: '1003', type: 'Blueberry' },
      { id: '1004', type: 'Devils Food' }
    ]
  }
}

p item.dig(:batters, :batter, -> { it[:type] == 'Chocolate' }, :id)
# => "1002"
# https://github.com/moraki-finance/ruby-experian/blob/84f7def9987b6377f4718a0730fdb564d6e9a0fb/lib/experian/trade_report.rb#L89
value_section&.find { |d| d["Tipo"] == value_name }&.dig("ListaValores", "Valor")&.find { |v| v["Periodo"] == period.to_s }&.dig("Individual")&.to_i
value_section&.dig(-> { it["Tipo"] == value_name }, "ListaValores", "Valor", -> { it["Periodo"] == period.to_s }, "Individual")&.to_i

# https://github.com/cyfronet-fid/marketplace/blob/f84947777aa79d02fb987092416a3cb143db3d01/lib/import/datasources.rb#L59
datasource_data&.dig("identifiers", "alternativeIdentifiers")&.find { |id| id["type"] == "PID" }&.[]("value")
datasource_data&.dig("identifiers", "alternativeIdentifiers", -> { it["type"] == "PID" }, "value")
  • で、これなんですがコメントでは次のようなパターンマッチでやりたいことが実現できる、と提示されていますね
item = {
  id: '0001',
  batters: {
    batter: [
      { id: '1001', type: 'Regular' },
      { id: '1002', type: 'Chocolate' },
      { id: '1003', type: 'Blueberry' },
      { id: '1004', type: 'Devils Food' }
    ]
  }
}

item => batters: { batter: [*, { type: "Chocolate", id: }, *] }
pp id
# => "1002"
  • [*, { type: "Chocolate", id: }, *] の部分がいわゆる find pattern と呼ばれているもので配列から { type: "Chocolate", id: } のパターンにマッチする要素を探し出してくる感じですね
  • パターンマッチに対応しているオブジェクトであればこっちのほうが書き味はよさそう
  • 一方でパターンマッチに対応していないオブジェクトやより複雑な条件の場合だとパターンマッチだとなかなかむずかしそうですかねえ
  • と、思ったけど以下のようにパターンには Proc も渡せるので思ったよりも柔軟性は高そうな気がする
item = {
  id: '0001',
  batters: {
    batter: [
      { id: '1001', type: 'Regular' },
      { id: '1002', type: 'Chocolate' },
      { id: '1003', type: 'Blueberry' },
      { id: '1004', type: 'Devils Food' }
    ]
  }
}

# マッチする type を Proc を用いてより複雑な条件で判定できる
item => batters: { batter: [*, { type: -> { _1.include?(" ") && _1.size > 10 }, id: }, *] }
pp id
# => "1004"
  • これはパターンマッチでは内部で #=== を呼び出してマッチするかどうか判定しているため
  • Proc#===Proc#call を呼び出すような挙動になっている
  • パターンマッチ便利すぎる…
GitHubで編集を提案

Discussion