Project

General

Profile

Actions

Feature #19272

closed

Hash#merge: smarter protocol depending on passed block arity

Added by zverok (Victor Shepelev) over 1 year ago. Updated about 1 year ago.

Status:
Rejected
Assignee:
-
Target version:
-
[ruby-core:111461]

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.

Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0