Accessing private member by subclassing


#1

Hi All,

SCENARIO: I took an example from Ruby Cookbook which had a private
method ‘secret’.
‘secret’ returned the value of @secret initialized to a random value.

I created a subclass whose initializer called super and accessed
secret to save its returned-value in a instance variable, which I then
displayed. It matched the value I dumped when the parent class was
initialized. Success!

However, I tried a alternative approach, which failed — which is my
reason for posting this.

I created a public method ‘spy’ in the parent class and returned the
value @secret, just like the private method ‘secret’ did. I then
invoked spy from a subclass instance, but it returned nil.

QUESTION: Why does subclass.spy return nil?

Code follows.

Thanks in Advance,
Richard

=========== SecretNumber.rb ====================

Source: Ruby Cookbook, p. 371-373

class SecretNum
def initialize
@secret=rand(20)
puts “@secret <= #{@secret}”
end

def hint
puts "The number is #{“not " if secret<10}greater than 10”
end

private
def secret
@secret
end

public
def spy
@secret
end
attr_reader :spy
end

=========== SecretNumber_AccessInSubclass.rb ===================

SecretNumber_AccessInSubclass.rb

require ‘SecretNumber.rb’

class NotSoSecretNum < SecretNum
def initialize
super
@theNum = secret
end

attr_reader :theNum

end

s = NotSoSecretNum.new
puts "theNum reveals “secret” = " + s.theNum.to_s
puts "secret reveals “secret” = " + ““s.secret” not accessible”
puts "spy reveals “secret” = " + s.spy.inspect.to_s

========= Results ==============
@secret <= 3
theNum reveals “secret” = 3
secret reveals “secret” = “s.secret” not accessible
spy reveals “secret” = nil


#2

Richard wrote:

def spy
@secret
end
attr_reader :spy

puts "spy reveals “secret” = " + s.spy.inspect.to_s
#=> nil

The problem looks like its with attr_reader. attr_reader creates that
accesses an instance variable. The instance variable and the method
have the same name. So basically, what you are doing is this:

#the method you wrote
def spy
@secret
end

#what attr_reader does for you
def spy
@spy
end

Now you have 2 methods named “spy”. When the second method called “spy”
is declared, the first one is overriden, it’s gone. And since @spy has
not been set to anything it just returns nil.

So get rid of:

attr_reader :spy

and it should work fine.


#3

Hi Alex,

Fantastic reply!

You not only told me what action to take to get my intended result,
but more importantly WHY my code failed. I never really gave much
thought to what attr_reader REALLY meant.

Thank you very much.

Best wishes,
Richard

On Feb 11, 7:14 pm, Alex W. removed_email_address@domain.invalid


#4

Hi again Alex,

After thinking further about my mistaken use of attr_reader, I
wondered why I had never learned the details about it before. So I
perused Hal F.'s “The Ruby Way, Second Edition” and found, on page
391, precisely the explanation you gave me. Clearly, I have not done
my homework :slight_smile:

I mention this primarily for the benefit of anybody else who might
read this thread.

Best wishes,
Richard

On Feb 11, 7:14 pm, Alex W. removed_email_address@domain.invalid


#5

Hi Alex,

Just one more question, if I may:

Hal F. say “attr_reader theNum” creates theNum [an instance
method, right?] and @theNum [an instance variable, right?].

However, the initialize method creates and initializes @theNum.

So, which of the following occurs?

(1) the initialize method creates/initializes @theNum first and
attr_reader acknowledges @theNum’s existence and restricts its
creation to theNum method; or

(2) the initialize method gets scanned but not executed, so
attr_reader does both creations and then the initialize method
initializes the existing @theNum instance variable.

The closest thing I’ve found on this issue is “compile” in Pickaxe 2nd
Edition, which applied to Regexp’s, not compiling/interpreting Ruby
source code.

Thanks in advance for any documentation you can point me to or any
ideas you have on this question.

Yours truly,
Richard

On Feb 11, 7:14 pm, Alex W. removed_email_address@domain.invalid


#6

Richard wrote:

Hi Alex,

Just one more question, if I may:

Hal F. say “attr_reader theNum” creates theNum [an instance
method, right?] and @theNum [an instance variable, right?].

I assume you mean:

attr_reader :theNum

However, the initialize method creates and initializes @theNum.

So, which of the following occurs?

(1) the initialize method creates/initializes @theNum first and
attr_reader acknowledges @theNum’s existence and restricts its
creation to theNum method; or

attr_reader reads only, not creates.

(2) the initialize method gets scanned but not executed, so
attr_reader does both creations and then the initialize method
initializes the existing @theNum instance variable.

You’re getting hung up on who creates what and has access to who and
it’s much simpler than that. Take this class:

class Foo
attr_reader :bar

def initialize(value)
  @bar = value
end

end

foo = Foo.new(‘abc’)
foo.bar #=> “abc”

When you call “Foo.new(‘abc’)” you are invoking the initialize method,
this sets the @bar variable. When you call “bar” you just retrieve
whatever the valid of @bar is at that moment.

And attr_reader gives you an instance method, meaning it’s only on
instances of class Foo created with: Foo.new(‘abc’). And in this
example you cannot instantiate the Foo class without running the
initialize method that sets up @bar.

So the “bar” method doesn’t acknowledge or restrict anything. It simply
fetches whats in @bar whether it’s there or not. As a result it doesn’t
care what else is going in your class at all. you can have a class like
this and it will work fine, except always return nil.

class Bar
attr_reader :foo
end

bar = Bar.new
bar.foo #=> nil

The final thing to keep in mind is that instance variable like @bar are
the only kind of variable that will never give you undefined or
uninitialized errors. So if it’s not initialized it will just give you
nil, unlike class of or local variables that will raise an exception:

irb(main):001:0> foo
NameError: undefined local variable or method `foo’ for main:Object
from (irb):1
from :0
irb(main):002:0> @foo
=> nil
irb(main):003:0> @@foo
NameError: uninitialized class variable @@foo in Object
from (irb):3
from :0

One last example:

class Bam
attr_reader :foo

def set_foo
  @foo = 'abc123'
end

end

bam = Bam.new
bam.foo #=> nil
bam.set_foo
bam.foo #=> “abc123”

I think all that answers your question.


#7

Hi Alex,

I responded earlier to your last post, but my response got lost in
cyber-space. Here is a new response, which hopefully is better
written than the lost one.

I assume you mean:

attr_reader :theNum

Yes, I omitted the colon :frowning: You’ve got eagle-eyes :slight_smile:

I think all that answers your question.

It sure does. Thanks a lot. I really like to nail down these kinds
of details.

I put together my list of examples based on the your ideas, which I
provided below. They satisfy me that:

  1. “attr_reader :foo” is equivalent to “def foo; @foo; end” (as you
    said originally)

I think examples 2a and b (and 4, 5 and 6, as well) demonstrate that.

  1. @foo” yields nil when it’s undefined. Occam’s razor suggests that
    a method returning @foo’s value:
    a) returns nil when @foo is undefined (as you also said); rather
    than
    b) creates @foo with an initial nil value when @foo is undefined, and
    then returns @foo’s nil value.

In short, I signed onto your ideas, and now proclaim them as mine,
too :-).

Best wishes,
Richard

===== My humble examples ====
class FooBar1
end

class FooBar2a
attr_reader :foo
end

class FooBar2b
def foo
@foo
end
end

class FooBar3
attr_reader :foo

def set_foo(parm)
@foo = parm
end
end

class FooBar4
def set_foo(parm)
@foo = parm
end

attr_reader :foo
end

class FooBar5
def set_foo(parm)
@foo = parm
end

def foo
@foo
end
end

class FooBar6
def set_foo(parm)
@foo = parm
end

def foo; @foo; end
end

fb1 = FooBar1.new

puts fb1.foo # ==> undefined method `foo’ for #<FooBar1: …

fb2a = FooBar2a.new
puts "fb2a.foo = " + fb2a.foo.inspect # ==> nil

fb2b = FooBar2b.new
puts "fb2b.foo = " + fb2b.foo.inspect # ==> nil

fb3 = FooBar3.new
puts "fb3.foo = " + fb3.foo.inspect # ==> nil
fb3.set_foo “some-value”
puts "fb3.foo = " + fb3.foo # ==> some-value

fb4 = FooBar4.new
fb4.set_foo “another-value”
puts "fb4.foo = " + fb4.foo # ==> another-value

fb5 = FooBar5.new
fb5.set_foo “a third value”
puts "fb5.foo = " + fb5.foo # ==> a third value

fb6 = FooBar6.new
fb6.set_foo “a fourth value”
puts "fb6.foo = " + fb6.foo # ==> a fourth value

On Feb 12, 12:32 pm, Alex W. removed_email_address@domain.invalid


#8

Richard wrote:

In short, I signed onto your ideas, and now proclaim them as mine,
too :-).

As long as we are being thorough, one last example that proves nil and
undefined are not the same thing when it comes to instance variables.

class Foo
attr_reader :bar

def set_bar(value)
  @bar = value
end

def bar_exists?
  defined? @bar
end

end

foo = Foo.new

foo.bar #=> nil
foo.bar_exists? #=> nil
foo.set_bar(“abc”)

foo.bar #=> “abc”
foo.bar_exists? #=> “instance-variable”

foo.set_bar(nil)
foo.bar #=> nil
foo.bar_exists? #=> “instance-variable”

This way you can tell if variable has been not been initialized, or if
has been initialized and was specifically set to nil.


#9

Hi Alex,

I got distracted by our snow-storm for a few days.

This way you can tell if variable has been not been initialized, or if
has been initialized and was specifically set to nil.

That’s excellent!!

Thank you very much for educating me in Ruby.

Yours very truly,
Richard

On Feb 12, 8:20 pm, Alex W. removed_email_address@domain.invalid