tags:

views:

645

answers:

4

I'm just learning ruby and trying to understand the scope of code executed in blocks. For example, I want to be able to create a block that affects the method that it is attached to, like so:

def test(&block)
  block.call() if block_given?
  puts "in test, foo is #{foo}"
  puts "in test, bar is #{bar}"
end

test() {
  foo="this is foo"
  bar="this is bar"
}

In this case I don't want to have to modify the block at all -- I want to be able to write it using simple variable references and no parameters. Only by making changes to the 'test' method in the above example, is it possible to access the variables defined in the block?

Again, the goal is to leave the block unmodified, but be able to access the created variables from within 'test' after the block executes.

+5  A: 

First of all, block.call() is done with yield, and you don't need the &block parameter that way.

You can't normally do what you want, blocks are binded when they are created, and inside the block you can see the local variables defined at that moment, the easiest way to do what you want, wich is not how you will use blocks normally, is this:

def test()
  foo = yield if block_given?
  puts "in test, foo is #{foo}"
end

test() {
  foo="this is foo"
}

But that's only a side effect because foo is "returned" by the block. If you instead do this:

def test()
  foo = yield if block_given?
  puts "in test, foo is #{foo}"
end

test() {
  foo="this is foo"
  "ha ha, no foo for you"
}

You'll notice that it does something different.

Here's more magic:

def test(&block)
   foo = eval "foo", block.binding
   puts foo
   block.call
   foo = eval "foo", block.binding
   puts foo
end

foo = "before test"
test() {
  foo = "after test"
  "ha ha, no foo for you"
}

That would kind of work, but it breaks if you remove foo = "before test" because foo becomes a local variable in the block and does not exist in the binding.

Summary: you can't access local variables from a block, just the locals where the block was defined and the return value of the block.

Even this won't work:

def test(&block)
   eval "foo = 'go fish'", block.binding
   block.call
   bar = eval "foo", block.binding
   puts bar
end

because the foo in the binding is different from the local in the block (I didn't know this, thanks).

krusty.ar
A: 
def test(&block)
  foo = yield
  puts "in test, foo is #{foo}"
end

test { "this is foo" }

prints in test, foo is this is foo

The value of yield is the value of the block.

You can also pass parameters to yield, which then can be accessed by the block using |param,another| at the block's beginning.

Also, check out procs.

foo = "this is foo"
p = Proc.new { "foo is #{foo}" }
p.call

Prints "foo is this is foo"

def test(p) 
  p.call
end

test p

Prints "foo is this is foo"

def test2(p)
  foo = "monkey"
  p.call
end

test2 p

Prints "foo is this is foo"

dylanfm
This is misleading, you are not accessing the locals in the block as the question says, just the return value of the block.
krusty.ar
+1  A: 

No, a block can't affect local variables in the place where it's called.

Blocks in Ruby are closures, which means they capture the scope around them when they are created. The variables that are visible when you create the block are the ones it sees. If there were a foo and bar at the top of your code, outside any method, that block would change those when it was called.

Chuck
+2  A: 

You can do what you want by being a little more verbose:

class Test
  def foo(t)
    @foo = t
  end
  def bar(t)
    @bar = t
  end
  def test(&block)
    self.instance_eval &block if block_given?
    puts "in test, foo is #{@foo}"
    puts "in test, bar is #{@bar}"
  end
end

Test.new.test() {
  foo "this is foo"
  bar "this is bar"
}

You can create methods like attr_accessor that will generate apropriate setter (the foo and bar methods).

rkj