Search code examples
rubyenumerator

Mapping enumerators


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)

Solution

  • 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.