Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does closure work for Ruby's define_singleton_method?

Tags:

closures

ruby

This Ruby (2.2.3p173) code:

class A
    def msg
        "hello"
    end

    def p
        Proc.new { msg }
    end

    def lam
        lambda { msg }
    end

    def blk
        (1..3).map { msg }.join(" and ")
    end

    def d1(obj)
        obj.define_singleton_method(:say1) { msg }
    end

    def d2(obj)
        bound = msg # <== Why is this needed?
        obj.define_singleton_method(:say2) { bound }
    end
end

a = A.new

puts a.p.call

puts a.lam.call

puts a.blk

obj = Object.new

a.d1(obj)

begin
# Why does this fail?
puts obj.say1
rescue
puts "caught: #{$!}"
end

a.d2(obj)

puts obj.say2

produces this output:

hello
hello
hello and hello and hello
caught: undefined local variable or method `msg' for #<Object:0x00000001a20638>
hello

What makes the difference in d1 and d2? Why do all of the blocks see msg except for the one passed to define_singleton_method?

Update:

I think it comes down to this:

Proc bodies have a lexical scope like functions in Lisp and JavaScript, meaning that when Ruby encounters a free variable inside a proc body, its value is resolved within the context the proc was defined. This is what makes closures possible. Methods are different, however. A method's context is the object to which they are bound. When Ruby encounters a free variable inside a method body, it assumes the variable refers to another method on the object, and this is what saves us from having to prefix same-object methods with "this" or "self".

which I found here: Of closures, methods, procs, scope, and Ruby.

All of these various blocks with { msg } must work as Proc body does. define_singleton_method is taking the block and giving it method's rules.

like image 751
Jim Flood Avatar asked Nov 08 '22 16:11

Jim Flood


1 Answers

The answer has to do with self and scopes

There are three ways to create a new scope in ruby. Classes, Modules, and Methods.

Your class creates a scope, and each of your methods create a scope that contain bindings specific to them. Closures are special though. A closure will grab the bindings that are around when you define the block and the block specific bindings disappear after the block ends. For example:

def my_method
  #Method scope
  x = "Goodbye"
  yield("Cruel")
end

x = "Hello"
#Closure says "I am going to grab the local bindings from my scope
my_method {|y| "#{x}, #{y} world" }

When ever you write the code

obj.define_singleton_method(:say1) { msg }

The only local bindings the closure grabs is 'obj' This can be demonstrated by modifying the code like so:

def d2(obj)
   puts "in the scope of method :d2, I have acces to the :msg method: #{methods.include?(:msg)}"
   puts "---"

    obj.define_singleton_method(:say2) do 
      puts "in the scope of this closure, I have acces to the :msg method: #{methods.include?(:msg)}"
      puts "Things I do have access to: "
      puts methods
      puts local_variables
    end
  end

A simple print statement of the most important part of ruby, self, will show you that you are operating in different scopes. Check out the code below:

def d2(obj)
   puts "in the scope of method :d2, I am operating as #{self}"
   puts "---"

    obj.define_singleton_method(:say2) do 
      puts "in the scope of this closure, I am operating as #{self}"
    end
end

So in short, the reason is because of scope. Whenever you declare bound = msg you are making the contents of msg local to the method and then the closure then can pick up the local binding value of msg.

If you want to read more about how this works, I highly recommend "The Pragmatic Programmers - Metaprogramming Ruby" You will learn a lot about self, and closures. http://www.amazon.com/Metaprogramming-Ruby-Program-Like-Facets/dp/1941222129

----EDIT---- "Why is"

def p Proc.new { msg } end

different than

def d2(obj) obj.define_singleton_method(:say2) { msg } end

It is different because self inside the block is different. Inside the method definition "p", the block has access to instance variables and methods, whereas the method "d2" has a block that only has access to Object. We can prove this with a little monkeypatching. Add this code:

class Object def msg "GoodBye" end

like image 65
Schylar Avatar answered Nov 15 '22 11:11

Schylar