6

I'm working my way through Dave's upcoming book on Elixir, and in one exercise I would like to dynamically construct a function reference to Kernel.+/2, Kernel.-/2 etc, based on the contents of one character of a string, '+', '-' and so on.

Based on another SO question I expected to be able to call apply/3 passing Kernel, :+ and the two numbers like this:

apply(Kernel, :+, [5, 7])

This doesn't work because (if I understand right) Kernel.+/2 is a macro, not a function. I looked up the source code, and + is defined in terms of __op__, and I can call it from iex:

__op__(:+, 5, 7)

This works until I put the :+ into a variable:

iex(17)> h = list_to_atom('+')
:+
iex(18)> __op__(h, 5, 7)
** (CompileError) iex:18: undefined function __op__/3
    src/elixir.erl:151: :elixir.quoted_to_erl/3
    src/elixir.erl:134: :elixir.eval_forms/4

And I'm guessing there's no way to call __op__ using apply/3.

Of course, the brute-force method gets the job done.

  defp _fn(?+), do: &Kernel.+/2
  defp _fn(?-), do: &Kernel.-/2
  defp _fn(?*), do: &Kernel.*/2
# defp _fn(?/), do: &Kernel.//2   # Nope, guess again
  defp _fn(?/), do: &div/2        # or &(&1 / &2) or ("#{div &1, &2} remainder #{rem &1, &2}")

But is there something more concise and dynamic?


José Valim nailed it with his answer below. Here's the code in context:

  def calculate(str) do 
    {x, op, y} = _parse(str, {0, :op, 0})
    apply :erlang, list_to_atom(op), [x, y]
  end

  defp _parse([]     , acc      )                 , do: acc
  defp _parse([h | t], {a, b, c}) when h in ?0..?9, do: _parse(t, {a, b, c * 10 + h - ?0})
  defp _parse([h | t], {_, _, c}) when h in '+-*/', do: _parse(t, {c, [h], 0})
  defp _parse([_ | t], acc      )                 , do: _parse(t, acc)
1

1 Answer 1

14

You can just use the Erlang one:

apply :erlang, :+, [1,2]

We are aware this is confusing and we are studying ways to make it or more explicit or more transparent.

UPDATE: Since Elixir 1.0, you can dispatch directly to Kernel (apply Kernel, :+, [1, 2]) or even use the syntax the OP first attempted (&Kernel.+/2).

Sign up to request clarification or add additional context in comments.

3 Comments

By the way, what would have been the proper incantation for the / macro, since &Kernel.//2 doesn't work?
This should do it: &(Kernel./)/2 (or &(&1 / &2))
If you have the operator as a single-quoted string, use List.to_atom(operator) as the second argument to apply

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.