2

How can I replace add_entry method with something more sensible?

class MyStorageClass

    def add_entry key, value
        eval "(@#{key} ||= []) << value; def #{key}; @#{key}; end"
    end

end

So then I can retrieve value as follows:

def get_entry key
    begin
        self.send key.to_sym
    rescue NoMethodError
        nil
    end
end

4 Answers 4

6

Rather than an instance variable per key, which requires some unnecessarily bulky code, why not just a single Hash like below. Also, define_method and define_singleton_method can be your friend to avoid bad bad evals.

class MyStorageClass
  def initialize
    @data = {}
  end

  def add_entry(key, value)
    (@data[key] ||= []) << value
    define_singleton_method(key){ @data[key] }
  end

  def get_entry(key)
    @data.key?(key) or raise NoMethodError
    @data[key]
  end
end

You may want to check that you're not overriding a predefined method first ([email protected]?(key) && self.respond_to?(key) at the top of the add_entry method would do), but that's for another conversation. Could be bad if someone tried to add a key called inspect, class, or, oh, get_entry, for example!

Sign up to request clarification or add additional context in comments.

8 Comments

Danger! It would have to be Hash.new{|h, k| h[k] = []}, or else all new keys gain values linked to the others. The [] object you passed is a single object in memory that will be referenced by all keys, and the << operator we are using modifies that object in place. Try it out! h = Hash.new([]); h[:x] << 1; h[:y] gives you [1]. I usually opt against the block initializer for Hash unless it's more beneficial than saving an ||=
You did! Sorry, I'm blind. But that's worse, without the h, k arguments, because you aren't storing it in the hash object at all! h = Hash.new{[]}; h[:x] << 1; h[:x] gives []
I suppose the danger still exists that someone confuses the curly braces for normal parens and the problem is perpetuated
From the docs "It is the block’s responsibility to store the value in the hash if required."
There's no need for your first line: "Edit:...". It's an unnecessary distraction. I would also put the boot to "IMO". Answers should not be opinions.
|
1

This can be achieved using instance_variable_set and attr_accessor:

class MyStorageClass
  def add_entry(key, value)
    if respond_to?(key)
      key << value
    else
      instance_variable_set("@#{key}", [value])
      self.class.send(:attr_accessor, key)
    end
  end
end

However as others have suggested, a cleaner approach is to simply use a Hash rather than defining a new instance method for every variable.

Comments

1

IMO this is a Really Bad Idea. Do not do this! You will be adding complexity with very little benefit.

I recommend instead an OpenStruct. These are great objects -- you can call getters and setters on them at will without specifying the attributes in advance. Perhaps a little inefficient, but that usually doesn't matter.

A side benefit of OpenStruct is that you can group your attributes into logical sets, e.g. connection_options, formatting_options, etc. Here's a sample script to illustrate:

#!/usr/bin/env ruby

require 'ostruct'

class MyClass

  attr_reader :config_options # only if you want to expose this

  def initialize
    @config_options = OpenStruct.new
  end

  def do_something
    config_options.color = 'yellow'
    config_options.size = 'medium'
  end

  def to_s
    config_options.to_h.to_s
  end
end

my_class = MyClass.new
my_class.do_something
puts my_class  # outputs: {:color=>"yellow", :size=>"medium"}

2 Comments

@mudasobwa Context/elaboration, please?
I just wanted to drop a link to the great library, that is [IMHO] by all means a better equivalent of OpenStruct.
0

I am not sure what you call “more sensible,” but here is the template without evals to start with:

def add_entry key, value
  # define instance variable unless it is already defined
  instance_variable_set :"@#{key}", [] \
    unless instance_variable_defined? :"@#{key}"
  # add value to the array
  instance_variable_set :"@#{key}", instance_variable_get(:"@#{key}") + value
  # define getter
  self.class.send :define_method key { instance_variable_get :"@#{key}" } \
    unless self.class.instance_methods.include?(key)
end

The getter might be defined in more readable manner:

  self.class.send :attr_reader, key \
    unless self.class.instance_methods.include?(key)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.