Constant Lookup with instance_eval in Ruby 1.9

2019-05-10 04:32发布

The Short and Sweet

Running this code in Ruby 1.9:

FOO = "global constant"

class Something
  FOO = "success!"

  def self.handle &block
    self.new.instance_eval &block
  end
end

class Other
  FOO = "wrong constant"

  def self.handle
    Something.handle{FOO}
  end
end

puts Something.handle{FOO}
puts Other.handle

I get "success!" and "wrong constant". How can I get both calls to print "success!"? This is not a contrived exercise for fun - I wouldn't waste people's time for that. I have a real-world problem, and I've whittled it down to the simplest possible example that demonstrates the issue. Read on for the "why".

A More Thorough Explanation

Calling Something.handle{FOO} works correctly. Ruby uses the definition of FOO given in the Something class. However, If I try to call it the same way from another class, it gives me the definition of FOO for that class.

I thought the idea of tighter constant looksups in Ruby 1.9 was to avoid problems like this. instance_eval is supposed to use the scope of its receiver (self.new, in this case), not the scope of the calling block. It works for things like instance variables, but not for constants. This is not a problem of precedence - remove the "global" and "wrong" constants, and ruby still won't be able to find the remaining correct constant.

The real-world problem I'm having is that I have a module with several classes. I have a method that accepts a block, and runs the block in the context of the module. Inside that block, I want to be able to refer to those classes by their short names (like I can anywhere in the module itself), rather than having to prepend the module name.

This is painful:

ThirdPartyApis::MyAnswerSite.connection question.id, answer.id do |question_id, answer_id|
  question = ThirdPartyApis::MyAnswerSite::Question.find question_id
  answer = ThirdPartyApis::MyAnswerSite::Answer.find answer_id

  ThirdPartyApis::MyAnswerSite::Solution.new question, answer
end

This is pleasant:

ThirdPartyApis::MyAnswerSite.connection question.id, answer.id do |question_id, answer_id|
  question = Question.find question_id
  answer = Answer.find answer_id

  Solution.new question, answer
end

Summary

So that's the long-winded explanation. Please don't offer workarounds that don't address my question. I appreciate the desire to explore other avenues, but my question is simple: can ONLY the Something class by modified, in a way that prints "success!" twice at the end? That's the test case, and any solution that fits this is my accepted answer, and its author is my personal hero for the week. Please!

1条回答
放荡不羁爱自由
2楼-- · 2019-05-10 05:12

I got it to work using the sourcify gem (gem install sourcify):

require 'sourcify'
FOO = "global constant"

class Something
  FOO = "success!"

  def self.handle &block
    (self.new.instance_eval(block.to_source)).call
  end

end

class Other
  FOO = "wrong constant"

  def self.handle
    Something.handle{FOO}
  end
end

puts Something.handle{FOO}
=> success!
puts Other.handle
=> success!

edit: Oops, you could see where I was working on using bindings too, removed that code.

查看更多
登录 后发表回答