Monday, March 12, 2007

Ruby: LocalJumpError workaround

I was recently working on some code very similar to the following distilled example.
class Foo

attr_accessor :some_val

def self.method_with_additional_behavior(method_name, &block)
define_method method_name do
# do some stuff before calling block
instance_eval &block
# do some stuff after calling block
end
end

method_with_additional_behavior :foo do
return 0 if self.some_val.nil?
some_val * 2
end
end

Foo.new.foo
Executing the above code gives the following error.
LocalJumpError: unexpected return
The error occurs because the return statement attempts to return from the method_with_additional_behavior. There are a few ways to solve this, but I also had some constraints that influenced my decision.

In my codebase the method_with_additional_behavior is defined in a superclass, and the subclasses use this method to create methods with additional behavior. I (believe I) could have forced them to use lambda and gotten around the problem, but that wouldn't be very friendly. I could also define another method, such as "__#{method_name}__", and call the underscored method from the desired method, but that would have left my classes with an additional method that should never be called in isolation.

I solved the problem by defining a method, storing that method in a local variable and then overriding the newly defined method with my additional behavior and a call to the method stored in the local variable.
class Foo

attr_accessor :some_val

def self.method_with_additional_behavior(method_name, &block)
define_method method_name, &block
new_method = instance_method(method_name)
define_method method_name do
# do some stuff before calling block
new_method.bind(self).call
# do some stuff after calling block
end
end

method_with_additional_behavior :foo do
return 0 if self.some_val.nil?
some_val * 2
end
end

Foo.new.foo #=> 0

4 comments:

  1. Why not just use alias_method_chain, is there some magic in the block that I'm missing?

    ReplyDelete
  2. Anonymous4:03 PM

    alias_method_chain would also (theoretically) work, but it would leave me with artifacts (methods that I would never call in isolation).

    I think the alias_method_chain solution is more conceptually easy to follow though, so you could make an argument for it.

    ReplyDelete
  3. Scott Taylor6:44 AM

    Well, this seems to work well for you, although it doesn't seem as though you've escaped the lambda problem (calling bind(self) gives you a lambda, after all).

    I had the same problem when using define_method to define instance methods on a class. I had to compromise. My original method looked something like this:

    def include_month?(month)
    self.each_as_time do |time|
    return true if time.month == month
    end
    false
    end

    The compromise looked something like this:
    sym = :month

    define_method "include_#{sym}?" do |month|
    truth_value = false
    self.each_as_time do |time|
    truth_value = true if time.month == month
    end
    truth_value
    end

    Makes for ugly code, IMHO. Anyway, thanks for the tip.

    BTW, Where is alias_chain_method method defined?

    ReplyDelete
  4. ZeusTheTrueGod2:04 AM

    Hi, I am new to ruby, but like lambda in other languages

    Here is my approach

    class Foo

    attr_accessor :some_val

    def self.method_with_additional_behavior(method_name, block)
    define_method method_name do
    # do some stuff before calling block

    result = block.call(self)
    p result
    # do some stuff after calling block
    end
    end

    method_with_additional_behavior :foo, lambda { |s|
    return 0 if s.some_val.nil?
    p s
    #s.some_val * 2
    }
    end

    Foo.new.foo

    ReplyDelete

Note: Only a member of this blog may post a comment.