💭
[Feature #21358] #digにProcを渡せるようにして、より複雑な条件で値を抽出できるようにする提案
[Feature #21358] Advanced filtering support for #dig
-
#digにProcを渡せるようにして、より複雑な条件で値を抽出できるようにする提案 - 例えば次のようなデータから
batterのtype: "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"
-
#findで呼び出すってことは複数の値にマッチする場合は『最初の要素』を抽出する感じになるんですかね? - いくつか実例が提示されているんですがまあまあこういうことをやりたいことが多いのかな?
- https://github.com/search?q=%2F(\w\[\w%2B%3F\]|\.(fetch|dig|at|\[\w%2B%3F\])\(.%2B%3F\))%26%3F%5C.(find%7Cdetect)%20%5C%7B%2F%20lang%3Aruby&type=code&p=1
# 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を呼び出すような挙動になっている - パターンマッチ便利すぎる…
Discussion