Project

General

Profile

Feature #19272

Updated by zverok (Victor Shepelev) almost 2 years ago

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, &block) 
     else 
       old_merge(other) { |_, o, n| block.call(o, n) } 
     else 
       old_merge(other, &block) 
     end 
   end 
 end 

 {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, 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 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.

Back