Как динамически вызвать оператора в Эликсире

Я работаю над предстоящей книгой Дейва об Эликсире, и в одном упражнении я хотел бы динамически создать ссылку на функцию для Kernel.+/2, Kernel.-/2 и т. д., основываясь на содержимом одного символа строки, '+', '-' и т. д.

Основываясь на другом вопросе SO, я ожидал, что смогу вызвать apply/3, передавая ядро, :+ и два числа, подобные этому:

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

Это не работает, потому что (если я правильно понимаю) Kernel.+/2 — это макрос, а не функция. Я посмотрел исходный код, и + определяется в терминах __op__, и я могу вызвать его из iex:

__op__(:+, 5, 7)

Это работает, пока я не помещу :+ в переменную:

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

И я предполагаю, что нет никакого способа вызвать __op__, используя apply/3.

Конечно, метод грубой силы делает свою работу.

  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}")

Но есть ли что-то более лаконичное и динамичное?


Хосе Валим прибил его своим ответом ниже. Вот код в контексте:

  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)

person Daniel Ashton    schedule 12.01.2014    source источник
comment
(не слишком актуально для вашего вопроса) мое решение сильно отличается от вашего! gist.github.com/nielsbom/0d689e00d927cc057ad7c5595d7e8f06   -  person Niels Bom    schedule 18.03.2019


Ответы (1)


Вы можете просто использовать Erlang:

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

Мы понимаем, что это сбивает с толку, и мы изучаем способы сделать это более явным или более прозрачным.

ОБНОВЛЕНИЕ: Начиная с Elixir 1.0, вы можете отправлять напрямую в ядро ​​(apply Kernel, :+, [1, 2]) или даже использовать синтаксис, который OP впервые попытался (&Kernel.+/2).

person José Valim    schedule 12.01.2014
comment
Кстати, какое заклинание было бы правильным для макроса /, если &Kernel.//2 не работает? - person Daniel Ashton; 14.01.2014
comment
Это должно сделать это: &(Kernel./)/2 (или &(&1 / &2)) - person José Valim; 14.01.2014
comment
Если у вас есть оператор в виде строки в одинарных кавычках, используйте List.to_atom(operator) в качестве второго аргумента для применения - person Jordan Dimov; 23.02.2015