Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do instance variables seemingly disappear when inside a block?

Forgive me, guys. I am at best a novice when it comes to Ruby. I'm just curious to know the explanation for what seems like pretty odd behavior to me.

I'm using the Savon library to interact with a SOAP service in my Ruby app. What I noticed is that the following code (in a class I've written to handle this interaction) seems to pass empty values where I expect the values of member fields to go:

create_session_response = client.request "createSession" do   soap.body = {     :user => @user, # This ends up being empty in the SOAP request,     :pass => @pass  # as does this.   } end 

This is despite the fact that both @user and @pass have been initialized as non-empty strings.

When I change the code to use locals instead, it works the way I expect:

user = @user pass = @pass  create_session_response = client.request "createSession" do   soap.body = {     :user => user, # Now this has the value I expect in the SOAP request,     :pass => pass  # and this does too.   } end 

I'm guessing this strange (to me) behavior must have something to do with the fact that I'm inside a block; but really, I have no clue. Could someone enlighten me on this one?

like image 622
Dan Tao Avatar asked Jul 19 '11 16:07

Dan Tao


People also ask

Where do instance variables go?

All instance variables are stored within the object, on the heap.

How long do instance variables last?

Instance variables persist as long as there are variables referencing them. PHP internally stores a reference count on each object. When an variable goes out of scope, PHP decrements the refcount and checks for 0. If it's 0, it cleans up the instance and destroys the object.

When a method finishes local variables disappear?

When the function terminates, the contents of local variables are lost. static local variables retain their contents between function calls. static local variables are defined and initialized only the first time the function is executed. 0 is the default initialization value.

Are instance variables always public?

Instance variables are always visible in all methods of the class. They always refer to the invocation object.


1 Answers

First off, @user is not a "private variable" in Ruby; it is an instance variable. Instance variables are available within the the scope of the current object (what self refers to). I have edited the title of your question to more accurately reflect your question.

A block is like a function, a set of code to be executed at a later date. Often that block will be executed in the scope where the block was defined, but it is also possible to evaluate the block in another context:

class Foo   def initialize( bar )     # Save the value as an instance variable     @bar = bar   end   def unchanged1     yield if block_given? # call the block with its original scope   end   def unchanged2( &block )     block.call            # another way to do it   end   def changeself( &block )     # run the block in the scope of self     self.instance_eval &block   end end  @bar = 17 f = Foo.new( 42 ) f.unchanged1{ p @bar } #=> 17 f.unchanged2{ p @bar } #=> 17 f.changeself{ p @bar } #=> 42 

So either you are defining the block outside the scope where @user is set, or else the implementation of client.request causes the block to be evaluated in another scope later on. You could find out by writing:

client.request("createSession"){ p [self.class,self] } 

to gain some insight into what sort of object is the current self in your block.

The reason they "disappear" in your case—instead of throwing an error—is that Ruby permissively allows you to ask for the value of any instance variable, even if the value has never been set for the current object. If the variable has never been set, you'll just get back nil (and a warning, if you have them enabled):

$ ruby -e "p @foo" nil  $ ruby -we "p @foo" -e:1: warning: instance variable @foo not initialized nil 

As you found, blocks are also closures. This means that when they run they have access to local variables defined in the same scope as the block is defined. This is why your second set of code worked as desired. Closures are one excellent way to latch onto a value for use later on, for example in a callback.

Continuing the code example above, you can see that the local variable is available regardless of the scope in which the block is evaluated, and takes precedence over same-named methods in that scope (unless you provide an explicit receiver):

class Foo   def x     123   end end x = 99  f.changeself{ p x } #=> 99 f.unchanged1{ p x } #=> 99 f.changeself{ p self.x } #=> 123 f.unchanged1{ p self.x } #=> Error: undefined method `x' for main:Object 
like image 193
Phrogz Avatar answered Sep 17 '22 14:09

Phrogz