Paul
Paul

Reputation: 36319

How do I work around Ruby's eval and "Already initialized constant"?

I inherited maintenance on an app that uses eval() as a way to evaluate rules written in Ruby code in a rules engine. I know there are a lot of other ways to do it, but the code base so far is pretty big, and changing it to something else would be prohibitive time-wise at this point; so assume I'm stuck using eval() for the moment.

The rules as written typically call up some of the same objects from the database as each other, and the rules writer gave the variables in the rules the same names as each other. This is resulting in pages and pages of "already initialized constant" warnings in the console during development.

I'm wondering a couple things:

First, if feels like those are slowing down the execution of the program in the dev environment, and so I'm wondering if it's a big performance hit in the production environment, specifically, having those warnings pop, not eval() itself, which I know is a hit.

Second, is there any way to "namespace" the execution of each rules so that it's not defining its variables on the same scope as all the other evals in the request to avoid that warning popping all over the place? I know I could rewrite all the rules to use ||= syntax or to check if a name has already been defined, but there's quite a lot of them so I'd rather do it from the code that runs the eval()'s, if possible.

** update with example rule ** A question has a rule about when it's to be displayed to a user. For example, if the user has stated that they live in an apartment, another question might need to be shown to ask what size the apartment building is. So the second question's rule_text might look like:

UserLivesInApartment = Question.find_by_name "UserLivesInApartment"
UserLivesInApartment.answer_for(current_user)

The code that calls the eval ensures there's a current_user variable in scope prior to evaluating.

Upvotes: 1

Views: 752

Answers (1)

sunkencity
sunkencity

Reputation: 3472

uh, eval is not perhaps the most golden of standards. You could probably fire up a drb instance and run the stuff in that instead of eval, that way you would have at least some control of what is happening and not pollute your own namespace.

http://segment7.net/projects/ruby/drb/introduction.html


Edit: added another answer for running the code in the same process:

I don't know how your rule code looks, but it might be possible to wrap a module around it:

# create a module
module RuleEngineRun1;end
# run code in module
RuleEngineRun1.module_eval("class Foo;end")

# get results
#....

# cleanup
Object.send(:remove_const, :RuleEngineRun1)

You can also create an anonymous module with Module.new { #block to be module eval'd } if you need to run code in parallel.


In later rubies you can add -W0 to run your code without printing warnings, but doing so makes possible errors go unnoticed:

$ cat foo.rb 
FOO = :bar
FOO = :bar

$ ruby foo.rb 
foo.rb:2: warning: already initialized constant FOO

$ ruby -W0 foo.rb 

You could also run your eval inside a Kernel.silence_warnings block, but might be devastating as well if you actually run into some real problems with the eval'd code, see Suppress Ruby warnings when running specs

Upvotes: 1

Related Questions