user1934428 February 2016

Ruby: evaluate string with dynamic binding of variables

I have a database of "formulas" stored as strings. Let's assume for simplicity, that each formula contains 2 variables denoted by a and b, and that the formulas are all wellformed and it is ensured that it consists only of characters from the set ()ab+-*.

At runtime, formulas are fetched from this database, and from another source, numeric values for a and b are fetched, and the formulas are evaluated. The evaluation can be programmed like this:

# This is how it works right now
formula = fetch_formula(....)
a = fetch_left_arg(....)
b = fetch_right_arg(....)
result = eval(formula)

This design works, but I'm not entirely happy with it. It requires that my program names the free variables exactly the same as they are named in the formula, which is ugly.

If my "formula" would not be a string, but a Proc object or Lambda which accepts two parameters, I could do something like

# No explicitly named variables
result = fetch_proc(...).call(fetch_left_arg(....),fetch_right_arg(....))

but unfortunately, the formulas have to be strings.

I tried to experiment in the following way: What if the method, which fetches the formula from the database, would wrap the string into something, which behaves like a block, and where I could pass parameters to it?

# This does not work of course, but maybe you get the idea:
block_string = "|a,b| #{fetch_formula(....)}"

Of course I can't eval such a block_string, but is there something similar which I could use? I know that instance_eval can pass parameters, but what object should I apply it to? So this is perhaps not an option either....

Answers


mudasobwa February 2016

This is very nasty approach, but for simple formulas you’ve mentioned it should work:

▶ formula = 'a + b'
▶ vars = formula.scan(/[a-z]+/).uniq.join(',')  # getting vars names
#⇒ "a,b"
▶ pr = eval("proc { |#{vars}| #{formula} }") # preparing proc
▶ pr.call 3, 5
#⇒ 8

Here we rely on the fact, that parameters are passed to the proc in the same order, as they appear in the formula.


sawa February 2016

If I get your question correctly, it is something that I have done recently, and is fairly easy. Given a string:

s = "{|x, y| x + y}"

You can create a proc by doing:

eval("Proc.new#{s}")


Wand Maker February 2016

You can create a lambda from string, as shown below:

formula = "a + b"
lambda_template = "->(a,b) { %s }"

formula_lambda = eval(lambda_template % formula)
p formula_lambda.call(1,2)
#=> 3


matt February 2016

One way to avoid creating the variables in the local scope could be to use a Binding:

bind = binding
formula = fetch_formula(....)
bind.local_variable_set :a, fetch_left_arg(....)
bind.local_variable_set :b, fetch_right_arg(....)
result = bind.eval(formula)

The variables a and b now only exist in the binding, and do not pollute the rest of your code.

Post Status

Asked in February 2016
Viewed 2,047 times
Voted 14
Answered 4 times

Search




Leave an answer