5
\$\begingroup\$

I had an assignment recently to build a FixedArray using Ruby. It's supposed to simulate a FixedArray in C, that is, an array that allocates a certain amount of arbitrary memory when initialized, and that this amount of memory can neither increase nor decrease. It cannot dynamically resize itself like Ruby's default Array (because at a low-enough level, there's no such thing as arrays that can resize themselves...only FixedArrays). If you try to access an index that is out of bounds, you get an exception.

Note that I cannot use Ruby's default Array (since that's kinda misses the point of the assignment, in my opinion), so you get something like this:

class OutOFBoundsException < RuntimeError
end

class FixedArray
  attr_reader :size

  def initialize(size)
    @size = size
    size.times do |index|
      self.instance_variable_set(:"@index#{index}", nil)
    end
  end

  def get(index)
    if instance_variable_defined?(:"@index#{index}")
      self.instance_variable_get("@index#{index}")
    else
      raise OutOFBoundsException
    end
  end

  def set(index, value)
    self.instance_variable_set(:"@index#{index}", value)
    value
  end

end

This seems rather hacky to me though. I'm having to use metaprogramming in order to create FixedArrays, and metaprogramming ought to be used as a last resort. Is there a better solution that can allow me to create FixedArrays?

\$\endgroup\$
  • 1
    \$\begingroup\$ If I understand you correct it would be no solution to use an Array and extend the class, so you would not be able to use indexes outside the range? (And the same thing: It would not be ok to use a kind of Hash? \$\endgroup\$ – knut Feb 19 '17 at 22:35
  • \$\begingroup\$ That's correct, @knut. \$\endgroup\$ – Left SE On 10_6_19 Feb 19 '17 at 22:37
  • \$\begingroup\$ this begs the question, what is the point of this exercise? Ruby is not designed to work like this. \$\endgroup\$ – Chris Drappier Feb 20 '17 at 6:52
  • \$\begingroup\$ @ChrisDrappier, later on, we would use this FixedArray as the building block for building other data structures like ArrayLists and LinkedLists. Since there's no such thing as a FixedArray in Ruby, we have to build our own. (You're right that Ruby was not designed to work like this, but we only knew Ruby and did not know C, so we work with what tools we have.) \$\endgroup\$ – Left SE On 10_6_19 Mar 3 '17 at 16:01
1
\$\begingroup\$

Given that you can't (or, rather, won't) use any existing collection-type classes (Array, Hash), which would be the simplest approach by far, I'd say your solution is ok. You could do a linked list implementation or something (a common way to implement a collection structure), but I'll leave such experiments to you.

So sure, it's kinda hacky, but it works.

It can be cleaned up a little, though.

Your OutOFBoundsException has a typo (OF rather than Of), and I'd prefer the suffix Error, as it inherits from an *Error class.

However, the more appropriate class to inherit from is probably IndexError. Which you could actually just use directly; no need to subclass. Slightly more generic than saying out-of-bounds, but still a good fit.

If you do want to subclass it anyway, I'd suggest nesting it under FixedArray rather than in the global scope, just to minimize footprint.

As for the FixedArray class itself, if we're keeping the array-elements-as-instance-variables approach, I'd suggest the following:

  • Implement the customary [] and []= element accessor methods (and use the latter in the initializer to set the nil values rather than repeat the instance_variable_set logic in two places)
  • Extract the numeric-index-to-variable-name logic into a private method to avoid duplicating how that string is built, and to have a single place to check for out-of-bounds
  • Define an each method and include Enumerable to get a bunch of stuff for free (like to_a, map, and so forth).

I'd also include some sanity-checking of the size argument given to the initializer; it has to be an integer, and it must be >= 0. Checking for out-of-bounds is reliant on the bounds being sensible, after all.

Another small thing: instance_variable_set already returns the value that's being set, so there's no need for the extra line to return value.

And you might want to add a to_s and inspect methods for good measure, but I'll leave that to you (you can just delegate them to to_a and let Array handle it, if you want).

I end up with something like this:

class FixedArray
  # This can be skipped in favor of using IndexError directly
  class OutOfBoundsError < IndexError; end

  include Enumerable

  attr_reader :size

  def initialize(size)
    raise ArgumentError if !size.is_a?(Integer) || size < 0
    @size = size
    size.times { |i| self[i] = nil }
  end

  def [](i)
    instance_variable_get(name_for_index(i))
  end

  def []=(i, value)
    instance_variable_set(name_for_index(i), value)
  end

  def each(&block)
    size.times { |i| yield self[i] }
  end

  private

  def name_for_index(index)
    raise OutOfBoundsError unless (0...size).include?(index)
    "@index#{index}"
  end
end

Semantically, you might say that name_for_index should raise an ArgumentError if the index it's given isn't an integer at all. Saying out-of-bounds is a little misleading. However, saying IndexError would make sense whether the index is out-of-bounds or not a number at all, so that might be a reason to use IndexError directly instead of adding a custom out-of-bounds error class.

Last thing about errors/exceptions: Add a useful description. I haven't done so in the code above, but it's definitely worth saying why, say, a ArgumentError occurred in the initializer rather than just raising it with no explanation.

\$\endgroup\$
  • 1
    \$\begingroup\$ It is generally not good Ruby style to require an object to be of a specific class. As long as an object responds to the right messages the right way, it should be usable. (This is called duck typing.) In the very rare cases that you really must have an object of a specific class, you should at least give that object an opportunity to convert itself to an appropriate object. E.g. Ruby's Array class is usually implemented as a machine array and thus size and indices must be machine integers, but even then, it doesn't actually require them to be Integers, it is enough to supply an … \$\endgroup\$ – Jörg W Mittag May 15 '17 at 8:55
  • \$\begingroup\$ … object which responds to to_int. So, I would rather have something like begin size = size.to_int rescue NoMethodError; raise TypeError end; raise ArgumentError if size <= 0; in initialize. \$\endgroup\$ – Jörg W Mittag May 15 '17 at 8:57
  • 1
    \$\begingroup\$ @JörgWMittag Good point. Duck typing would be a nicer solution. The above was written to be as "direct" as possible, but you're right \$\endgroup\$ – Flambino May 15 '17 at 9:21
2
\$\begingroup\$

I'm not sure that delegating to a normal array is not the best solution. Normally when I create classes like this the intent is to provide a more rigid interface over an array and delegation would be the best solution. What you have will certainly work but is going to be extremely in-efficient speed wise since those get/set/defined methods are quite slow. i.e. This is great as an exercise but I would never write code like this in a real world app.

Some changes I would make:

  • The set method does no checking so it is currently possible to add items to the array
  • Instead of instance_variable_defined? I would use (0..size).include?(index)
  • I would extract the index checking into a method and use it in both set/get:

    def check_index!(index)
      raise OutOfBoundsError unless (0..size).include?(index)
    end
    
  • I would add [] and []= aliases for get and set

Also most style guides recommend against using self. redundantly to prefix method names.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.