views:

63

answers:

2

I noticed (and verified in the sunspot code) the following behavior

class Foo < ActiveRecord::Base
  def  bar
    search_str = "foo"
    Boo.search do
      keywords(search_str)
      p self.id
      p self
   end
 end
end

In the code above, the DSL block can access the variables defined in the context. But the self inside block, points to an instance of Sunspot::DSL::Search class (instead of an instance of Foo class.) When I try to access self.id, instead of getting the id of a Foo object; I get the id of a Sunspot::DSL::Search object.

I think Sunpot is doing some binding swapping/delegation magic in Util.instance_eval_or_call method.

I am curious why Sunspot does this and why there is no warning about this behavior in documentation.

Edit:

The Sunspot search method can be found at this link

The code below will illustrate my point. In the method foo I have block that behaves as expected. In the method bar, the block doesn't behave.

class Order < ActiveRecord::Base  

  def foo
    p self.class.name # prints Order

    # The `self` inside the block passed to the each method
    # points to an object of type Order (as expected)
    # This is the normal block behavior.
    [1,2,3].each do |val|
      p self.class.name # prints Order
    end
  end


  def bar

    p self.class.name # prints Order

    # the `self` inside the block passed to the search method
    # points to an object of type Sunspot::DSL::Search.
    # This is NOT the normal block behavior.

    Order.search do
      keywords("hello")
      p self.class.name # prints Sunspot::DSL::Search
    end
end

Note2

I have located the code in Sunspot source tree that modifies the normal block behavior. My question is about the reason for rigging the binding like this.

Note3

Specifically, I found an issue while invoking the id method in side the block. The search method delegates the method invocation inside the block to the DSL object and if it doesn't find the method then call is re-delegated to the calling context. Search method strips all but the essential methods from the DSL object before registering delegation code. The id method is not stripped out. This is causing the problem. For all the other methods delegation works fine.

This behavior is not documented in the Sunspot method documentation.

+1  A: 

Isn't it just an instance_eval? Unless you're talking about accessing instance variables from the calling context, this is normal closure behaviour.

I'm assuming the instance_eval (the change in self) is used to provide keywords and other related methods to the block.

banister
No, it is not the standard `instance_eval`. The sunspot library comes with a utility class and the method is in that class.
KandadaBoggu
In a typical block `self` is set the value of `self` during the creation of block. The behavior in Sunspot DSL block is not standard.
KandadaBoggu
@Kandada, your example code shows nothing more than the behaviour of an ordinary `instance_eval`. Can you provide example code that demonstrates behaviour that is not explained by an `instance_eval`? or provide a link to the implementation of the method you claim is so special. ;)
banister
@Kandada, after looking at the code on github for SunSpot, I agree it's not a straight forward `instance_eval`. See my new answer. However, your example did not demonstrate this behaviour. :)
banister
Updated the question.
KandadaBoggu
+2  A: 

Ok, I know how it works:

The magic is found in ContextBoundDelegate in util.rb.

  • It creates a blank slate delegator object.
  • The delegator forwards all method calls to the 'receiver'. In your example the 'receiver' is probably the object which contains the methods keywords and with and any_of and so on.
  • If a given method is not found in 'receiver' then it forwards the method call onto the 'context' object
  • The context object is the object that holds the bindings for the block.
  • You find the context object for a given block by doing this: eval('self', block.binding)

Rationale:

So the effect of all this is that the block not only gets access to the methods in the search object (a la instance_eval) but it also gets access to local methods in the calling scope of the block.

The block also, of course, gets access to local variables in the calling scope of the block, but this is just normal closure behaviour.

The block does not, however, get access to instance variables in the calling scope of the block.

The following code might be useful as it follows roughly the same idea but is much simpler and less sophisticated: http://stackoverflow.com/questions/3690305/using-methods-from-two-different-scopes/3691511#3691511

banister
+1 for actually locating the file.. :-)
KandadaBoggu