nickcoxdotme
nickcoxdotme

Reputation: 6707

Elixir: Dynamic infix operator

I'm trying to write a simple module function that takes an expression as a string like "123 + 45" and returns an answer. So:

Calculator.calculate("123 + 45")
# => 168

I thought about just splitting the string on (" ") to get the integers and the operator, then basically calling Code.eval_string on it. Here was my first naive attempt:

defmodule Calculator do
  def calculate(str) do
    [x, oper, y] = String.split(str, " ")
    formula = "a = fn (c, d) -> c operator d end; a.(c, d)"
    {answer, _ } = Code.eval_string(formula, [
      c: String.to_integer(x),
      operator: String.to_atom(oper),
      d: String.to_integer(y)
    ])
    answer
  end
end

Then, in running it, I receive this error:

** (CompileError) nofile:1: undefined function c/1
    (elixir) src/elixir_fn.erl:10: anonymous fn/3 in :elixir_fn.expand/3
    (stdlib) lists.erl:1239: :lists.map/2
    (elixir) src/elixir_fn.erl:14: :elixir_fn.expand/3

I couldn't figure out why c was being evaluated as a function. I suspect that it has to do with the operator variable in the anonymous function. I confirmed that by rewriting it with a hardcoded operator:

defmodule Calculator do
  def calculate(str) do
    [x, _, y] = String.split(str, " ")
    formula = "a = fn (c, d) -> c + d end; a.(c, d)"
    {answer, _ } = Code.eval_string(formula, [
      c: String.to_integer(x),
      d: String.to_integer(y)
    ])
    answer
  end
end

Indeed, this produces the expected result. The question is:

Why did the presence of the operator variable binding in the anonymous function cause c to be evaluated as a function?

It appears from the docs on Code.eval_string makes it seem as though the variable bindings can be just about anything as long as they're found in the keyword list that is the second argument to eval_string.

In my second attempt, I thought about trying to convert the operator from the input string into an atom and convert the operator from an infix to a function call (i.e., from 1 + 3 to something like 1.(:+, [3]). But that doesn't appear to be valid syntax.

So my second question is:

Is it possible to write an expression with an infix operator such as + as a function, and then to be able do dynamically define that operator as an atom?

Upvotes: 0

Views: 456

Answers (1)

Dogbert
Dogbert

Reputation: 222348

Why did the presence of the operator variable binding in the anonymous function cause c to be evaluated as a function?

The code c operator d is parsed as c(operator(d)) by Code.eval_string, just like it would in normal Elixir, which means both c and operator must be functions of arity 1 for that expression to make sense. You wouldn't expect the following code to work, would you?

c = 1
operator = :+
d = 2
a = fn (c, d) -> c operator d end; a.(c, d)

Is it possible to write an expression with an infix operator such as + as a function, and then to be able do dynamically define that operator as an atom?

Since these operators are just functions defined in Kernel module, you can use apply/3 to call it:

iex(1)> c = 1
1
iex(2)> operator = :+
:+
iex(3)> d = 2
2
iex(4)> apply(Kernel, operator, [c, d])
3

So in your original code, just replace c operator d with apply(Kernel, operator, [c, d]).

There's also no need of eval_string here.

iex(1)> [a, b, c] = String.split("123 * 456", " ")
["123", "*", "456"]
iex(2)> a = String.to_integer(a)
123
iex(3)> b = String.to_atom(b)
:*
iex(4)> c = String.to_integer(c)
456
iex(5)> apply(Kernel, b, [a, c])
56088

Upvotes: 3

Related Questions