0

I am trying to create dynamic methods for a class so that the methods are hash key names using define_method. Can anyone suggest why I can't pass my hash as an argument to the class constructor? When I check through pry the @my_hsh variable I get nil. Why am I getting nil and not my hash? When I run this code, I have an error that that nil does not have an each method

# frozen_string_literal: true

require 'pry'

class Foo
  def initialize(my_hsh = {})
    @my_hsh = my_hsh
  end
  
  @my_hsh.each do |k, v|
    define_method k do
      v
    end
  end
end

hsh = {
  first: 1,
  second: 2,
}
my_foo = Foo.new(hsh)
my_foo.first
my_foo.second
CekyndA
  • 9
  • 1
  • 2
    It looks like the hash iteration is not happening within the `initialize` method. You may also want to use `define_singleton_method` there instead as the methods defined will vary based on the hash passed to the instance. – Zoran Aug 19 '21 at 18:44
  • 2
    The 1st `@my_hsh` within `initialize` is an instance variable whereas the 2nd `@my_hsh` in the class body is a class instance variable. Instance and class are different objects with their own set of instance variables. – Stefan Aug 19 '21 at 18:49

3 Answers3

1

@my_hsh is nil because it is being called before the initialize method is run.

@my_hsh = my_hsh is run when the object is initialised

the @my_hash.each loop is being called when the class is being initialialised, which is before Foo.new is called.

To make this work you would need to create a method on the Foo class itself then call that method against the food class.

e.g.

# frozen_string_literal: true

class Foo
  def self.define_methods(hash)
    hash.each do |k, v|
      define_method k do
        v
      end
    end
  end
end

hsh = {
  first: 1,
  second: 2,
}

Foo.define_methods(hsh)
my_foo = Foo.new
puts my_foo.first
puts my_foo.second

If you really want to define the methods on instantiation you could do it 2 ways

  1. Add the methods to the class on object instantiation
# frozen_string_literal: true

class Foo
  def initialize(my_hsh = {})
    my_hsh.each do |k, v|
      self.class.define_method k do
        v
      end
    end
  end
end


hsh = {
  first: 1,
  second: 2,
}
my_foo = Foo.new(hsh)
puts my_foo.first
puts my_foo.second

this is not ideal, because if you create a second instance it will affect the first instance

e.g.

class Foo
  def initialize(my_hsh = {})
    my_hsh.each do |k, v|
      self.class.define_method k do
        v
      end
    end
  end
end


hsh = {
  first: 1,
  second: 2,
}
hsh2 = {
  first: 3,
  second: 4,
}
my_foo = Foo.new(hsh)
Foo.new(hsh2)
puts my_foo.first # 3
puts my_foo.second # 4

alternatively you could make the methods dynamic by using method_missing

class Foo
  def initialize(my_hsh = {})
    @hsh = my_hsh
  end

  def method_missing(method)
    @hsh[method] or raise NoMethodError.new("Method `#{method}` doesn't exist.")
  end
end


hsh = {
  first: 1,
  second: 2,
}
hsh2 = {
  first: 3,
  second: 4,
}
my_foo = Foo.new(hsh)
Foo.new(hsh2)
puts my_foo.first # 1
puts my_foo.second # 2
puts my_foo.three # NoMethodError raised
  • 1
    "@my_hsh is nil because it is being called before the initialize method is run." – This is wrong. The problem is simply that the OP is setting the instance variable on one object, then trying to read it from a completely different object. – Jörg W Mittag Aug 20 '21 at 08:37
  • i already did this task with method_missing, but I would like to understand why this code does not work. Thanks for the explanation. – CekyndA Aug 20 '21 at 11:50
0

You could check singleton methods: What exactly is the singleton class in ruby? and if it fits for you, you can implement in this way.

Note that the loop for method definition needs to be wrapped up in another method and called this is an example:

class Foo
  def initialize(my_hsh = {})
    @my_hsh = my_hsh
    self.define_methods_from_hash
  end
  
  def define_methods_from_hash
    @my_hsh.each do |k, v|
      define_singleton_method k do
        v
      end
    end
  end
end

Then you can check if it works:

hsh = {
  first: 1,
  second: 2,
}
my_foo = Foo.new(hsh)
p my_foo.first #=> 1
p my_foo.second #=> 2
p my_foo.singleton_methods #=> [:second, :first]

While without passing any hash:

my_foo_2 = Foo.new() # default empty hash
p my_foo_2.singleton_methods #=> []
iGian
  • 11,023
  • 3
  • 21
  • 36
0

Why am I getting nil and not my hash?

In your class definition, you have two different variables @my_hsh : The one in the initializer pretains to the instance of your class. The one written a bit below belongs to the class itself. You try it in irb with this example:

 class C; @x=5; def initialize; @x=13; end; def f; @x; end; def self.g; @x; end; end

If you now request the value of C.new.f, you get 13, but if you request C.g, you get 5.

I always try to name class and instance variables differently, even though it is not required; having the same name just leads to confusion and errors.

user1934428
  • 19,864
  • 7
  • 42
  • 87