From: "zverok (Victor Shepelev) via ruby-core" Date: 2023-02-07T07:49:56+00:00 Subject: [ruby-core:112261] [Ruby master Feature#19272] Hash#merge: smarter protocol depending on passed block arity Issue #19272 has been updated by zverok (Victor Shepelev). @nobu All of my examples work with my reference implementation. You can try it yourself. `:any_symbol.to_proc.arity` is `-2`, corresponding to the following lambda: ```ruby ->(first, *rest) { first.send(symbol, *rest) } ``` The behavior is corresponding, too: ```ruby def fake_to_proc(symbol) = ->(first, *rest) { first.send(symbol, *rest) } :+.to_proc.arity #=> -2 fake_to_proc(:+).arity #=> -2 :+.to_proc.parameters #=> [[:req], [:rest]] fake_to_proc(:+).parameters #=> [[:req, :first], [:rest, :rest]] :+.to_proc.call(1) # `+': wrong number of arguments (given 0, expected 1) (ArgumentError) -- on handling +, not calling the lambda fake_to_proc(:+).call(1) # `+': wrong number of arguments (given 0, expected 1) (ArgumentError) :+.to_proc.call(1, 2) #=> 3 fake_to_proc(:+).call(1, 2) #=> 3 ``` Therefore: * Any `:+.to_proc.arity` is -2 * Which is not a bug/accident, but a proper reporting of arity/parameters * Which actually made me think about this idea with `merge` :) * Which works with the reference implementation. ---------------------------------------- Feature #19272: Hash#merge: smarter protocol depending on passed block arity https://bugs.ruby-lang.org/issues/19272#change-101690 * Author: zverok (Victor Shepelev) * Status: Open * Priority: Normal ---------------------------------------- Usage of `Hash#merge` with a "conflict resolution block" is almost always clumsy: due to the fact that the block accepts `|key, old_val, new_val|` arguments, and many trivial usages just somehow sum up old and new keys, the thing that should be "intuitively trivial" becomes longer than it should be: ```ruby # I just want a sum! {apples: 1, oranges: 2}.merge(apples: 3, bananas: 5) { |_, o, n| o + n } # I just want a group! {words: %w[I just]}.merge(words: %w[want a group]) { |_, o, n| [*o, *n] } # I just want to unify flags! {'file1' => File::READABLE, 'file2' => File::READABLE | File::WRITABLE} .merge('file1' => File::WRITABLE) { |_, o, n| o | n } # ...or, vice versa: {'file1' => File::READABLE, 'file2' => File::READABLE | File::WRITABLE} .merge('file1' => File::WRITABLE, 'file2' => File::WRITABLE) { |_, o, n| o & n } ``` It is especially noticeable in the last two examples, but the _usual_ problem is there are too many "unnecessary" punctuation, where the essential might be lost. There are proposals like #19148, which struggle to define _another_ method (what would be the name? isn't it just merging?) But I've been thinking, can't the implementation be chosen based on the arity of the passed block?.. Prototype: ```ruby class Hash alias old_merge merge def merge(other, &block) return old_merge(other) unless block if block.arity.abs == 2 old_merge(other) { |_, o, n| block.call(o, n) } else old_merge(other, &block) end end end ``` E.g.: **If, and only if, the passed block is of arity 2, treat it as an operation on old and new values.** Otherwise, proceed as before (maintaining backward compatibility.) Usage: ```ruby {apples: 1, oranges: 2}.merge(apples: 3, bananas: 5, &:+) #=> {:apples=>4, :oranges=>2, :bananas=>5} {words: %w[I just]}.merge(words: %w[want a group], &:concat) #=> {:words=>["I", "just", "want", "a", "group"]} {'file1' => File::READABLE, 'file2' => File::READABLE | File::WRITABLE} .merge('file1' => File::WRITABLE, &:|) #=> {"file1"=>5, "file2"=>5} {'file1' => File::READABLE, 'file2' => File::READABLE | File::WRITABLE} .merge('file1' => File::WRITABLE, 'file2' => File::WRITABLE, &:&) #=> {"file1"=>0, "file2"=>4} # If necessary, the old protocol still works: {apples: 1, oranges: 2}.merge(apples: 3, bananas: 5) { |k, o, n| k == :apples ? 0 : o + n } # => {:apples=>0, :oranges=>2, :bananas=>5} ``` As far as I can remember, Ruby core doesn't have methods like this (that change implementation depending on the arity of passed callable), but I think I saw this approach in other languages. Can't remember particular examples, but always found this idea appealing. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/