Search code examples
rubyhashgeneratorenumerator

Infinite Enumerable


I'm building a hash with keys available at runtime (so the size of the object isn't known beforehand). I want all these values to be a new instance of a class ContestStanding, but not the exact same instance. I've achieved this with

h = Hash.new {|h,k| h[k] = ContestStanding.new}
@my_keys.map {|k| h[k]}
h #=> {1=>#<ContestStanding...>, 2=>#<ContestStanding...>, ...}

I'm wondering if there's a way I could do this using Enums or Lambdas like the following. Note: I've verified this does not work. This is just my thought process

Hash[@my_keys.zip(-> { ContestStanding.new })]

Here, the problem is my Lambda isn't enumerable. Is there something like an infinite generator in Ruby?

EDIT

I initially got really tripped up by that Enumerable#each_with_object method. Didn't see the order of k and h in the block parameters. Thought I was going crazy! As for your suggested implementation, when I run in IRB, this is what I get

my_keys = [1,2,3]
my_keys.each_with_object({}) {|k,h| h[k] = 'a'}
#=> {1=>"a", 2=>"a", 3=>"a"}
# The above is what I want to get out of the implementation
Hash[my_keys.zip(Array.new(my_keys.size, Hash.new {|h,k| h[k] = 'a'}))]
#=> {1=>{}, 2=>{}, 3=>{}}

I'm not looking for a Hash of Hashes. That seems to be what the implementation is returning. I'm wanting to get back {1=>'a', 2=>'a', 3=>'a'}. Any thoughts on that?


Solution

  • Brad,

    Here are two ways you could produce the hash. I will use the following as an example:

    class ContestStanding
      def checkit
        puts "hi"
      end
    end
    
    my_keys = [1,2,3]
    

    Use Enumerable#each_with_object

    h = my_keys.each_with_object({}) { |k,h| h[k] = ContestStanding.new }
      #=> {1=>#<ContestStanding:0x000001010efdd8>,
      #    2=>#<ContestStanding:0x000001010efdb0>,
      #    3=>#<ContestStanding:0x000001010efd88>}
    h[1].checkit #=> "hi"
    

    each_with_object creates and empty array which is referenced by the block parameter h. The first value passed into the block (and assigned to the block parameter k) is my_keys.first => 1, so have

    h[1] = ContestStanding.new
    

    The other elements of the hash are created similarly.

    Use Array.zip

    Hash[my_keys.zip(Array.new(my_keys.size) {ContestStanding.new})]
      #=> {1=>#<ContestStanding:0x0000010280f720>,
      #    2=>#<ContestStanding:0x0000010280f6f8>,
      #    3=>#<ContestStanding:0x0000010280f6d0>}
    

    or, for Ruby v2.0+

    my_keys.zip(Array.new(my_keys.size) {ContestStanding.new}).to_h
      #=> {1=>#<ContestStanding:0x0000010184bd48>,
      #    2=>#<ContestStanding:0x0000010184bd20>,
      #    3=>#<ContestStanding:0x0000010184bcf8>}
    

    Here the following steps are performed:

    a = Array.new(my_keys.size) {ContestStanding.new}
      #=> [#<ContestStanding:0x0000010185b248>,
      #    #<ContestStanding:0x0000010185b220>,
      #    #<ContestStanding:0x0000010185b1f8>]
    b = my_keys.zip(a)
      #=> [[1, #<ContestStanding:0x0000010185b248>],
      #    [2, #<ContestStanding:0x0000010185b220>],
      #    [3, #<ContestStanding:0x0000010185b1f8>]]
    b.to_h
      #=> {1=>#<ContestStanding:0x0000010185b248>,
      #    2=>#<ContestStanding:0x0000010185b220>,
      #    3=>#<ContestStanding:0x0000010185b1f8>}
    

    Your solution

    I found your solution interesting. This is one one way of explaining how it works:

    enum = Enumerator.new { |y| loop { y << ContestStanding.new } }
      #=> #<Enumerator: #<Enumerator::Generator:0x000001011a9530>:each>
    a1 = my_keys.size.times.with_object([]) { |k,a| a << enum.next }
      #=> [#<ContestStanding:0x000001018820a0>,
      #    #<ContestStanding:0x00000101882028>,
      #    #<ContestStanding:0x00000101881fb0>
    a2 = my_keys.zip(a1)
      #=> [[1, #<ContestStanding:0x000001018820a0>],
      #    [2, #<ContestStanding:0x00000101882028>],
      #    [3, #<ContestStanding:0x00000101881fb0>]]
    Hash[a2]
      #=> {1=>#<ContestStanding:0x000001018820a0>,
      #    2=>#<ContestStanding:0x00000101882028>,
      #    3=>#<ContestStanding:0x00000101881fb0>}