Ruby reusable map's block

Viewed 94

I've got the following code that does the job:

pairs = {
  pair1: {
    first: [{a: 1}, {b: 2}, {c:3}],
    second: [{a: 1}, {b: 2}, {c:3}],
  },
  pair2: {
    first: [{a: 1}, {b: 2}, {c:3}, {d:4}],
    second: [{a: 1}, {b: 2}, {c:3}],
  },
  pair3: {
    first: [{a: 1}, {b: 2}, {c:3}],
    second: [{a: 1}, {b: 2}, {c:3}],
  }
}

pairs.flat_map { |_, t| t[:first] }.reduce Hash.new, :merge
pairs.flat_map { |_, t| t[:second] }.reduce Hash.new, :merge

I would like to know how it would be possible to make this more elegant by extracting common code into a method. Common code being map block. I did read up on Ruby's blocks but I had some difficult time understanding it and could use some help. My initial attempt was:

def reusable_map_block
  pairs.flat_map(yield).reduce Hash.new, :merge
end

reusable_map_block { |t| t[:first] }
def reusable_map_block(&block)
  pairs.flat_map(block.call).reduce Hash.new, :merge
end

reusable_map_block { |t| t[:first] }
def reusable_map_block(&block)
  pairs.flat_map(block.call).reduce Hash.new, :merge
end

pairs.reusable_map_block { |t| t[:first] }

But it's yet not clicking to me. This is just a syntax I'm experimenting with - ultimate goal is giving developer a control which hash to select for further processing and using function as an argument felt natural coming from JS/TS/.NET.

2 Answers

If you want this to be accessible on your pairs directly you need to add that method to the class. I've made a slight alteration since you'll need to use self instead of pairs which no longer exists:

class Hash
  def reusable_map_block(&block)
    values.flat_map(&block).reduce(Hash.new, :merge)
  end
end

If you're okay with that then you can get the result you want:

pairs.reusable_map_block { |t| t[:first] }

You may want to give this a more meaningful name like reduce_values or something of the sort.

In order to convert a block into a Proc object, you use the & unary prefix ampersand sigil in a parameter list, which will "roll up" the block into a Proc object and bind it to that parameter.

In order to convert a Proc object into a block, you use the & unary prefix ampersand operator in an argument list, which will "unroll" the Proc object into a block:

def reusable_map_block(pairs, &block)
  pairs.flat_map(&block).reduce Hash.new, :merge
end

reusable_map_block(pairs) { |_, t| t[:first] }

You can add this as an extension method to the Hash class if you want:

class Hash
  def reusable_map_block(&block)
    flat_map(&block).reduce Hash.new, :merge
  end
end

pairs.reusable_map_block { |_, t| t[:first] }

However, when monkey-patching, it is generally preferred to use a clearly named module, so that the monkey-patch shows up in the inheritance chain, and the name of the module gives you an indication for the name of the file you should look into:

module ReusableMapBlockExtension
  def reusable_map_block(&block)
    flat_map(&block).reduce Hash.new, :merge
  end
end

class Hash
  include ReusableMapBlockExtension
end

pairs.reusable_map_block { |_, t| t[:first] }

Even better yet, if you know that your code will only be run on Ruby implementations that support Refinements, you could use that:

module ReusableMapBlockExtension
  def reusable_map_block(&block)
    flat_map(&block).reduce Hash.new, :merge
  end
end

module HashWithReusableMapBlock
  refine Hash do
    include ReusableMapBlockExtension
  end
end

pairs.reusable_map_block { |_, t| t[:first] }
# NoMethodError (undefined method `reusable_map_block' for #<Hash:0x00007f8442831e68>)

using HashWithReusableMapBlock

pairs.reusable_map_block { |_, t| t[:first] }
#=> {a: 1, b: 2, c: 3, d: 4}
Related