Feature #19272
closedHash#merge: smarter protocol depending on passed block arity
Description
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:
# 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:
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:
{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.