3

Using an Enumerator in Ruby is pretty straightforward:

a = [1, 2, 3]
enumerator = a.map
enumerator.each(&:succ) # => [2, 3, 4]

But can I do something similar with nested collections?

a = [[1, 2, 3], [4, 5, 6]]
a.map(&:map) # => [#<Enumerator: [1, 2, 3]:map>, #<Enumerator: [4, 5, 6]:map>]

But now how do I get [[2, 3, 4], [5, 6, 7]]?

This could always be done with a block:

a = [[1, 2, 3], [4, 5, 6]]
a.map { |array| array.map(&:succ) } # => [[2, 3, 4], [5, 6, 7]]

But I was wondering if there was a way that avoided the use of a block, partly because I find it annoying to have to type |array| array and also partly because I'm curious to find a way to do it.

Ideally, it would feel like this psuedocode:

a.map.map(&:succ)
# perhaps also something like this
a.map(&:map).apply(&:succ)
Justin
  • 24,288
  • 12
  • 92
  • 142
  • See my update a previous SO Post suggested that something similar to your request can be supported (Although I still would not recommend it but for the sake of completeness it seemed worth mentioning) – engineersmnky Jul 30 '15 at 20:39
  • You could do this: `p = ->(e) { e.map(&:succ) }; a.map(&p) #=> [[2, 3, 4], [5, 6, 7]]`. – Cary Swoveland Jul 30 '15 at 20:56

2 Answers2

4

To my knowledge there is no specific implementation as per the way you requested it.

You could just create a recursive function to handle this such as:

def map_succ(a)
  a.map {|arr| arr.is_a?(Array) ? map_succ(arr) : arr.succ}
end

Then it will work no matter how deeply nested the Array's are (caveat if the elements do not respond to #succ this will fail).

If you really wanted to you could monkey_patch Array (IN NO WAY RECOMMENDED)

#note if the element does not respond to `#succ` I have nullified it here
class Array 
  def map_succ
    map do |a| 
      if a.is_a?(Array) 
        a.map_succ 
      elsif a.respond_to?(:succ) 
        a.succ
      #uncomment the lines below to return the original object in the event it does not respond to `#succ`
      #else
        #a 
      end
    end
  end
end

Example

a = [[1, 2, 3], [4, 5, 6], [7, 8, 9, [2, 3, 4]], {"test"=>"hash"}, "F"]
a.map_succ
#=> [[2, 3, 4], [5, 6, 7], [8, 9, 10, [3, 4, 5]], nil, "G"]

The nil is because Hash does not have a #succ method.

UPDATE

Based on this SO Post a similar syntax could be supported but note that recursion is still probably your best bet here so that you can support any depth rather than an explicit one.

 #taken straight from @UriAgassi's from post above
 class Symbol
   def with(*args, &block)
     ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
   end
 end

Then

 a = [[1,2,3],[4,5,6]]
 a.map(&:map.with(&:succ))
 #=> [[2, 3, 4], [5, 6, 7]]
 a << [7,8,[9,10]] 
 #=> [[2, 3, 4], [5, 6, 7],[7,8,[9,10]]]
 a.map(&:map.with(&:succ))
 #=> NoMethodError: undefined method `succ' for [9, 10]:Array
Community
  • 1
  • 1
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
4

The only way I know of doing this is to do the following:

a = [[1, 2, 3], [4, 5, 6]]
a.map { |b| b.map(&:succ) } # => [[2, 3, 4], [5, 6, 7]]

Mainly because of the combination of Array#map/Enumerable#map and Symbol#to_proc, you cannot pass a second variable to the block that #map yields for, and thus pass another variable to the inner #map:

a.map(1) { |b, c| c } # c => 1, but this doesn't work :(

So you have to use the block syntax; Symbol#to_proc actually returns a proc that takes any number of arguments (you can test this by doing :succ.to_proc.arity, which returns -1). The first argument is used as the receiver, and the next few arguments are used as arguments to the method - this is demonstrated in [1, 2, 3].inject(&:+). However,

:map.to_proc.call([[1, 2, 3], [4, 5, 6]], &:size) #=> [3, 3]

How? :map.to_proc creates this:

:map.to_proc # => proc { |receiver, *args, &block| receiver.send(:map, *args, &block) }  

This is then called with the array of arrays as an argument, with this block:

:size.to_proc # => proc { |receiver, *args, &block| receiver.send(:size, *args, &block) }

This results in .map { |receiver| receiver.size } being effectively called.

This all leads to this - since #map doesn't take extra arguments, and passes them to the block as parameters, you have to use a block.

Jeremy Rodi
  • 2,485
  • 2
  • 21
  • 40