From: "nobu (Nobuyoshi Nakada) via ruby-core" <ruby-core@...>
Date: 2023-02-07T01:41:07+00:00
Subject: [ruby-core:112241] [Ruby master Feature#19272] Hash#merge: smarter protocol depending on passed block arity

Issue #19272 has been updated by nobu (Nobuyoshi Nakada).


zverok (Victor Shepelev) wrote:
> 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}
> ```

`:+.to_proc` is a proc just calls `+` method on the first argument with the rest.
That means its arity is not deterministic.

> ```ruby
> {words: %w[I just]}.merge(words: %w[want a group], &:concat)
> #=> {:words=>["I", "just", "want", "a", "group"]}
> ```

In this example, you expect `Array#concat` on the old values, but the arity of `Array#concat` is -1 not 2.


----------------------------------------
Feature #19272: Hash#merge: smarter protocol depending on passed block arity
https://bugs.ruby-lang.org/issues/19272#change-101664

* 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/