Pickaxe question: "... on the way to true Ruby mastery."


#1

Pickaxe has this code on page 374:

def once(*ids) # :nodoc:
for id in ids
module_eval <<-“end;”
alias_method :#{id.to_i}, :#{id.to_s}
private :#{id.to_i}
def #{id.to_s}(*args, &block)
(@#{id.to_i} ||= [#{id.to_i}(*args, &block)])[0]
end
end;
end
end

followed by “Understand this code, and you’ll be well on the way to
true Ruby mastery.” I don’t, so I’m not, but would like to, and then I
will (according to the book) - with some help.

I’m good up until the line (@#{id.to_i} ||= [#{id.to_i}(*args,
&block)])[0]

Say the id.to_i was 42 … then this reads: (@42 ||= [42(*args,
&block)])[ 0 ]

or: (@42 ||= [ return value from 42 method call ] )[ 0 ]

Question: Why can’t we just write

(@42 ||= return value from 42 method call)

Why is the [0] necessary at all?

Thanks, (ps - a link to an in-depth explanation would be sufficient!)

Jeff


#2

My guess is it is necessary to keep the value in the array in case the
real
value is nil. Without it in an array, @#{id.to_i} will evaluate to
nil,
and the || operator will execute the next term, the function call. You
don’t
want to call the function again, your desired value is already in the
instance variable and it is nil.

(@#{id.to_i} ||= [#{id.to_i}(*args, &block)])[0]

-andre


#3

Hey Andre, I believe the line reads slightly differently than you’ve
interpreted … some more info (note: I still don’t have the answer):

||= is actually a ruby idiom to initialize a variable if it’s nil. In
other words, “a ||= 6” actually means “a = 6 if a.nil?” or “a = 6
unless a”. (sorry if I’m lecturing) In other words, if @foo really
is a value, and it really is nil, I think the function call is
going to be made no matter what … but again, this is a bit over my
head at present.

So, converting #{id.to_i} to something more readable, like “foo”,
this: (@foo ||= [foo(*a, &b)] )[ 0 ]

should be the same as the more verbose:

if @foo
return @foo[ 0 ] # Still not sure why the [0] is required!
else
return foo( *a, &b ) # the 0th index of the single array
end

jz


#4

Hi –

On Wed, 3 Jan 2007, removed_email_address@domain.invalid wrote:

So, converting #{id.to_i} to something more readable, like “foo”,
this: (@foo ||= [foo(*a, &b)] )[ 0 ]

should be the same as the more verbose:

if @foo
return @foo[ 0 ] # Still not sure why the [0] is required!
else
return foo( *a, &b ) # the 0th index of the single array
end

It’s more like:

if @foo
return @foo[0]
else
@foo = [foo(*a, &b)]
return @foo[0]
end

or, more simply,

@foo = [foo(*a, &b)] unless @foo
return @foo[0]

In other words, @foo gets assigned to, if it’s nil to start with.

David


#5

Hi Jeff,

I think this is the intention of the code, that the assignment function
is
called precisely only once and it is generic, in a sense that it can
store
any value, including nil and ‘false’

Now, you are correct that what you describe there is the behavior of the
code ‘to be replaced’ by ‘once’. The intention is the same, namely, the
first call returned value should be cached and the subsequence call
should
just return that cached value, never to assigned again.

The original code, however, has drawback: if the cached value is nil or
‘false’, the assignment will be made again. The ‘once’ code is an
improvement over the original code that 1) it’s more elegant, 2) and it
trully assigns only once, even with nil or ‘false’ cached value.

-andre


#6

Hi –

On Thu, 4 Jan 2007, removed_email_address@domain.invalid wrote:

Thanks again. It’s pretty obvious, actually … sigh.

Funny, I still don’t feel well on my way to mastery. I actually feel
dumber, in a way.

I’d say Dave T.'s characterization of it as non-obvious and
non-trivial is a good reality check in that regard :slight_smile: And I assure
you that, having studied this example in depth, there’s a lot of Ruby
code that you’ll take in stride now that you would have found opaque
before. Since you’ll take it in stride, you may not know that
you’re taking it in stride, if you see what I mean – and that’s what
keeps us eager to learn. (And “we” doesn’t mean just Ruby programmers
or even just programmers, but people in general who are learning
things.)

David


#7

Thanks David and Andre, I finally get it, and Andre, you’re right about
the nil/false idea in your response to my reponse.

For anyone else following this thread, the idea is:

  • The first time the method is called, @foo doesn’t exist.

  • @foo doesn’t exist, so it’s assigned a single-element array
    containing the value returned by the call to the private aliased
    original method. This value can be anything, including an array,
    false, nil, whatever.

  • The 0th array element, which is just the return value of the function
    call, is returned.

  • The next time the method is called, @foo still exists, and is still a
    single-element array. We again return the 0th element, which is the
    answer.

The [0] is used because the result is stored in an array … this gets
around the original method returning nil or false, which would end up
in a second call to the function, had we just naively used @foo ||=
foo( *args, &block )…

Thanks again. It’s pretty obvious, actually … sigh.

Funny, I still don’t feel well on my way to mastery. I actually feel
dumber, in a way.

jz


#8

removed_email_address@domain.invalid wrote:

false, nil, whatever.
in a second call to the function, had we just naively used @foo ||=
foo( *args, &block )…

Thanks again. It’s pretty obvious, actually … sigh.

Funny, I still don’t feel well on my way to mastery. I actually feel
dumber, in a way.

jz

Welcome to the wonderful world of meta-programming in Ruby! And don’t
feel dumb. As the saying goes: the more you know, the more you know
that you don’t know. Ask any true master of anything and that’ll be
the answer they give you; if they’re a >true< master.

Ken

PS. Check these out to continue your mastery…

http://www.erikveen.dds.nl/monitorfunctions/index.html
http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/22b97167fe614efc/2627d192d2bf56d5?hl=en#2627d192d2bf56d5