Vargas
Vargas

Reputation: 2155

Ruby: Create array items "on demand"

In Ruby with Hashes I can do this:

h = Hash.new { |h,k| h[k] = "created #{k}" }

so that every time I try to access an item with a key that does not exists in the Hash it will call the block and create this new item and store with the key.

Is there a similar way to do this with Arrays?

Upvotes: 1

Views: 138

Answers (2)

Matheus Moreira
Matheus Moreira

Reputation: 17020

The Array.new method can receive a block. It passes the index of the element and the result of the block is stored in the array.

Array.new(3) { |index| index ** 2 }
# => [0, 1, 4]

However, all the elements will be created the moment you call the method. They will also be stored in the block and there is no way to prevent that.

We can subclass Array and implement the desired Hash-like behavior.

class CustomArray < Array
  def [](index)
    if super.nil? then @default_proc.call self, index end
    super
  end
end

class << CustomArray
  def new(*arguments, **keyword_arguments, &block)
    if arguments.empty? and block
      super().tap do |array|
        array.instance_variable_set :@default_proc, block
      end
    else super end
  end
end

This way, the usual Array API is preserved. If a size parameter isn't passed with the block, it will be used as the default Proc.

array = CustomArray.new { |array, index| array[index] = index + 2 }

p array[10]
# => 12

p array
# => [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 12]

See it run here.

Note that this implementation is problematic due to the meaning of nil. In this case, it is defined to be the empty value due to how arrays work, which is reasonable in the context of contiguous memory. If you store an element at an index that is bigger than the size of the array, it will fill in the blanks with nil in order to indicate empty space between the last element and the element you just inserted.

If nil is a meaningful value, consider using a Hash with integer keys instead of an Array.

Upvotes: 2

Dani&#235;l Knippers
Dani&#235;l Knippers

Reputation: 3055

By default, no I do not think that is possible. If you are willing to do a little monkey-patching, you can add a similar method yourself. For example, by extending Array with a get method which accepts a Block, you can simulate what you want.

The get method will act like the regular [] when no Block is given. When you pass a Block and the value is nil, it will store whatever results from the Block at index i.

class Array
  def get(i, &block)
    return self[i] if !block || !self[i].nil?

    self[i] = block.call(i)
  end
end

array = [1, 2]
array.get(0) # => 1 

array.get(5) # => nil
array.get(5) { |i| "Created index #{i}" } # => "Created index 5"

p array # => [1, 2, nil, nil, nil, "Created index 5"] 

Upvotes: 1

Related Questions