1

With the following code, in what way can I access @arr from Child?

class Parent
    class << self
        def create_singleton_variable
            @arr = [1,2,3]
        end

        def arr
            @arr
        end
    end
end

class Child < Parent
    def get
        puts self.arr
    end
    def self.get 
        puts self.arr
    end
end


p "class method call #{Child.get}"
#=> ➜ ruby child.rb    
#=> "class method call "

c = Child.new
p "instance call #{c.get}"
#=> ➜ ruby child.rb 
#=> Traceback (most recent call last):
#=>        1: from child.rb:24:in `<main>'
#=> child.rb:15:in `get': undefined method `arr' for #<Child:0x00007fe0eb02e7d8> (NoMethodError)

I've tried many other ways as well, but don't feel the need to post them here.

edit to the question, since it appears I do need a bit more context:

I'm attempting to prepend a module into the Thor framework. I want to then access this bit of code

module ThorExtensions
  module Thor
    module CompletionGeneration
      def self.prepended(base)
        base.singleton_class.prepend(ClassMethods)
      end

      module ClassMethods
        def completion
          puts "Start Completion"
          p self
          p self.superclass
          p self.class.superclass.subcommands
          puts "End Completion"
        end
      end
    end
  end
end

results in

Start Completion
Debug
Thor
bundler: failed to load command: exe/pt (exe/pt)
NoMethodError: undefined method `subcommands' for Module:Class
  /Users/tyler.thrailkill/Documents/code/backend/pt-cli/lib/thor_extensions/completion_generation.rb:13:in `completion'
  /Users/tyler.thrailkill/Documents/code/backend/pt-cli/lib/debug/debug.rb:24:in `<class:Debug>'
  /Users/tyler.thrailkill/Documents/code/backend/pt-cli/lib/debug/debug.rb:4:in `<top (required)>'
  /Users/tyler.thrailkill/Documents/code/backend/pt-cli/lib/pt.rb:5:in `require'
  /Users/tyler.thrailkill/Documents/code/backend/pt-cli/lib/pt.rb:5:in `<top (required)>'
  exe/pt:13:in `require'
  exe/pt:13:in `<top (required)>'

which of course is not what I want. It appears that maybe my issue is with prepending?

Edit 2

I seem to have done a terrible job of explaining my issue with prepending. Here is a fully working example showing my issue. I believe this is due to how prepending something to a class essentially creates another Class in the call stack that is called first. My hope is that I'm actually still able to access this method somehow.

class Parent
  class << self
    def create_singleton_variable
      @arr = [1,2,3]
      puts "arr is initialized #{@arr}"
    end
    # ... lots of code here. 
    def arr
      puts "arr is #{@arr.inspect}"
      @arr
    end
  end
end

module CompletionGeneration
  def self.prepended(base)
    base.singleton_class.prepend(ClassMethods)
  end

  module ClassMethods
    def completion 
      puts "self.superclass.arr == #{self.superclass.arr.inspect}" # unable to access superclass arr
      puts "self.class.superclass.arr == #{self.class.superclass.arr}" # likewise, unable to access Child's metaclass superclass 
    rescue Exception => e
      # do nothing, this is just so you can see arr is actually initialized in the context of the Child
      p e
    end
  end
end

Parent.prepend CompletionGeneration

class Child < Parent
  create_singleton_variable
  completion
  arr
end

Child.new

results in the output

➜ ruby child.rb
arr is initialized [1, 2, 3]
arr is nil
self.superclass.arr == nil
#<NoMethodError: undefined method `arr' for Module:Class>
arr is [1, 2, 3]

This code should be simply copy and pastable as is.

4
  • 1
    In light of your edit I deleted my answer for now, but will revisit the question after I return from an outing with Saffi, my faithful canine companion. Commented Jan 7, 2019 at 18:13
  • I've found this, which is tantalizingly close to what I want. wiseheartdesign.com/articles/2006/09/22/…, but it uses includes, rather than prepends. I need to prepend (well maybe not need, but I'd like to) into Thor. I also can't access subcommands or the like until after they've been initialized by other class level methods. Commented Jan 7, 2019 at 18:25
  • Theoretically I thought I should be able to access any methods in Thor, but for some reason they don't seem to be the same 'context' as the regular thor calls, resulting in empty arrays or hashes being returned. I did forget to mention that I don't really have trouble calling the methods on the object, but they're never populated, I'm guessing due to that. Commented Jan 7, 2019 at 19:06
  • With more research, I'm actually able to access Thor::Base methods for some reason, but still not Thor class methods themselves. Commented Jan 7, 2019 at 20:03

1 Answer 1

1

Here is your code, slightly modified.

class Parent
  def self.create_singleton_variable
    @arr = [1,2,3]
  end 
  def self.arr
    puts "self = #{self} in the getter for @arr"
    @arr
  end
end

class Child < Parent
  def get
    puts self.arr
  end
  def self.get 
    puts self.arr
  end
end

I have written Parent in the more conventional way. Except for the addition of the puts statement, it is equivalent to that contained in the question.

First, a head-slapper: Kernel#puts-anything returns nil. You need to remove puts from both methods:

class Child < Parent
  def get
    self.arr
  end
  def self.get 
    self.arr
  end
end

Parent.create_singleton_variable
  #=> [1, 2, 3] 
Child.get.nil?
self = Child in the getter for @arr
  #=> true

We see that within the getter arr, invoked by Child's class method get, self equals Child, so the method looks for a class instance variable @arr of Child not of Parent. As no such instance variable has been initialized, nil is returned.

You need the following.

class Parent
  class << self
    def create_singleton_variable
      @arr = [1,2,3]
    end
    def arr
      puts "self = #{self} in the getter for @arr"
      @arr
    end
  end
end

class Child < Parent
  def get
    self.class.superclass.arr
  end
  def self.get 
    superclass.arr
  end
end

The crucial difference to that given in the question is that Class#superclass changes the scope (i.e., self) to Parent.

We see the desired result is obtained.

Child.get
self = Parent in the getter for @arr
  #=> [1, 2, 3] 

Child.new.class.superclass.arr
self = Parent in the getter for @arr
  #=> [1, 2, 3] 

A common misconception is that the Child class method defined def self.get; self.arr; end invokes the getter Parent::arr, and therefore returns the value of Parent's instance variable @arr. It is Child::arr that is invoked, however, that method having been inherited from Parent, and it is Child's class instance variable @arr that is being retrieved, a subtle, but important, distinction.

Edit 2

The first observation is that Parent can be written in the more conventional (and completely equivalent) way.

class Parent
  def self.create_singleton_variable
    @arr = [1,2,3]
    puts "arr is initialized #{@arr}"
  end

  def self.arr
    puts "arr is #{@arr.inspect}"
    @arr
  end
end

Regardless of how its written, self will equal Parent when either class method is involked on parent. The first, therefore, will create the class instance variables @arr.

Parent.methods(false)
  #=> [:create_singleton_variable, :arr] 
Parent.instance_variables
  #=> []
Parent.ancestors
  #=> [Parent, Object, Kernel, BasicObject] 

Now let's create a class variable for Parent.

Parent.create_singleton_variable
  # arr is initialized [1, 2, 3]
Parent.instance_variables
  #=> [:@arr]

Now let me change the value of @arr.

Parent.instance_variable_set(:@arr, ['dog', 'cat'])
  #=> ["dog", "cat"]
Parent.arr
  # arr is ["dog", "cat"]
  #=> ["dog", "cat"] 

Next, create the class Child, but do not yet prepend the module.

class Child < Parent
  create_singleton_variable
  arr
end
arr is initialized [1, 2, 3]
arr is [1, 2, 3]

Child.ancestors
  #=> [Child, Parent, Object, Kernel, BasicObject]
Child.instance_variables
  #=> [:@arr] 
Child.instance_variable_get(:@arr)
  #=> [1, 2, 3] 

There are no surprises. Next load the module.

module CompletionGeneration
  def self.prepended(base)
    base.singleton_class.prepend(ClassMethods)
  end

  module ClassMethods
    def completion
      puts "self=#{self}"
      puts "superclass=#{superclass}"
      puts "self.class=#{self.class}"
      puts "self.class.superclass == #{self.class.superclass}" 
      puts "superclass.arr == #{superclass.arr.inspect}"
      puts "self.class.superclass.arr == #{self.class.superclass.arr}" 
      rescue Exception => e
        # do nothing, this is just so you can see arr is actually
        # initialized in the context of the Child
        puts "Exception => e=#{e}"
    end
  end
end

(Note self. is not needed in "superclass.arr == #{superclass.arr.inspect}") Now prepend this module to Parent.

Parent.prepend CompletionGeneration

Parent.ancestors
  #=> [CompletionGeneration, Parent, Object, Kernel, BasicObject] 
Parent.methods.include?(:completion)
  #=> true 
Child.ancestors
  #=> [Child, CompletionGeneration, Parent, Object, Kernel, BasicObject] 
Child.methods.include?(:completion)
  #=> true 

The callback modules method CompletionGeneration::prepended is fired with base equal to Parent, causing Parent's singleton class to prepend ClassMethods, thereby adding the class method Parent::completion. Since Parent did not previously have a method by that name using prepend or include would have the same effect. Further, instead of Parent.singleton_class.include ClassMethods, one could have used the included(base) callback instead, and executed Parent.extend ClassMethods. Perhaps prepend is being used here for a general case where Parent may have a class method by that name.1

Now execute the following.

Child.completion
self=Child
superclass=Parent
self.class=Class
self.class.superclass == Module
arr is ["dog", "cat"]
superclass.arr == ["dog", "cat"]
Exception => e=undefined method `arr' for Module:Class

The exception was raised when

puts "self.class.superclass.arr == #{self.class.superclass.arr}"

was being executed. As that amounts to

puts "self.class.superclass.arr == #{Module.arr}"

but of course Module has no module method arr.

1 In view of Child.ancestors, prepending Parent with the module only causes Parent's children to include (rather than prepend) the module; that is, if a child already has a method completion before the prepending, that method will not be preempted by the the module's method by the same name.

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

10 Comments

Yes, sorry, my example was a bit too simplified for my actual use case sadly. So, accessing in a static context of course works, but I'm actually trying to prepend a module into a library to then access variables in this way.
I've edited my question to be a bit more clear about what I'm actually trying to do.
I seem to have made myself come across as an idiot. I understand all that, including the common misconception you stated. I've updated my question with a fully working example showing what I believe the actual issue to be (something with prepending). Yes I did have a few typos in my first example where I was printing stuff, but it shouldn't have mattered since I was printing in both locations, it would have printed at some point. This new example fully illustrates the issue with proper output, replicating exactly the issue I'm having.
And thank you for your time helping me with this. Bit frustrating is all. I believe the issue is that prepending essentially just adds a new class with methods to the bottom of the call stack, but what I don't know is what the solution to this problem is.
any clue on what I can do? I've succeeded at getting this working with monkey patching the relevant class, but I'd rather prepend it so I can pull it out into a library
|

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.