Эликсир - динамический вызов частной функции

Я нашел Kernel.apply/3, который позволяет динамически вызывать открытый метод в модуле, указав метод как атом, например. result = apply(__MODULE__, :my_method, [arg]) переводится на result = my_method(arg)

Что меня озадачивает - это способ вызова частного метода; Данный код выглядит следующим образом:

defmodule MyModule do
    def do_priv(atom, args) when is_list(args) do
        apply(__MODULE__, atom, args)
    end

    # (change defp => def, and this all works)
    defp something_private(arg), do: arg #or whatever
end

Я ожидал бы, что MyModule.do_priv(:something_private, [1]) допустимо, так как это вызов частного метода изнутри модуля. Я могу оценить, что под капотом Elixir использует Erlang apply/3, и поэтому этот подход, вероятно, не приведет нас туда.

Я также пробовал использовать метод Code.eval_quoted/3, но он даже не способен вызывать жесткокодированный частный метод (и, следовательно, время, затрачиваемое на создание AST вручную, вместо использования quote do как ниже - хотя это вариант, если кто-то видит, как это сделать):

defmodule MyModule do
    def do_priv_static do
        something_private(1) #this works just fine
    end

    def do_priv_dynamic do
        code = quote do
            something_private(1)
        end
        Code.eval_quoted(code, [], __ENV__)   #nope.  fails
    end

    defp something_private(arg), do: arg #or whatever
end

Опять же, он получает доступ к частной функции изнутри содержащего модуля, поэтому я ожидаю, что это будет допустимо. Возможно, что я просто не понимаю параметр __ENV__ для eval_quoted

Единственное рабочее решение прямо сейчас меняет defp на def, что является прекрасным решением для моего личного кода; но поскольку я пишу код, который поддерживает других программистов, которые заботятся, я бы хотел найти решение.

Я открыт для других подходов, но лично я не понимаю, как это сделать.

Ответ 1

Сначала вы должны знать, что f(), вызываемый внутри модуля MyModule, - это не то же самое, что MyModule.f(), вызываемое в том же месте. См. http://www.erlang.org/doc/reference_manual/code_loading.html#id86422

Вы можете использовать только частные функции f(). Эти вызовы также проверяются компилятором - если функция не существует, вы получаете ошибку компиляции. Когда вы используете MyModule.f() в одном и том же месте, вы не получаете ошибку компиляции, потому что эти вызовы проверяются только во время выполнения (даже если вы вызываете модуль из себя), и эффект (AFAIK) аналогичен тому, как если бы вы были вызывая MyModule.f() из любого другого модуля - модуль просматривается во время выполнения, и вы можете вызывать только экспортированные (общедоступные) функции.

Поэтому вы не можете вызывать частные функции любым другим способом, кроме простого f(). apply(mod,fun,[]) эквивалентен стилю mod.fun.() - модуль разрешен во время выполнения, а частные функции недоступны.

Вы можете попробовать все варианты самостоятельно в этом примере: https://gist.github.com/mprymek/3302ff9d13fb014b921b

Теперь вы можете видеть, что вызовы частных функций всегда должны быть известны во время компиляции, поэтому вы не можете использовать даже волшебство eval_quoted или любую другую магию, чтобы сделать их "динамическими"...

Рекомендация Sasa Juric по использованию @doc false - правильное решение.

Ответ 2

AFAIK динамически вызывает частные функции в Erlang (и, следовательно, не в Elixir). Если вам нужно выполнить динамическую отправку, вы можете рассмотреть возможность использования функции с несколькими предложениями. Надуманный пример (конечно, плохой, но не могу придумать лучший банкомат):

iex(1)> defmodule Math do
          def operation(op) do
            IO.puts "invoking #{inspect op}"
            run_op(op)
          end

          defp run_op({:add, x, y}), do: x + y
          defp run_op({:mul, x, y}), do: x * y
          defp run_op({:square, x}), do: x * x
        end

iex(2)> Math.operation({:add, 1, 2})
invoking {:add, 1, 2}
3

iex(3)> Math.operation({:mul, 3, 4})
invoking {:mul, 3, 4}
12

iex(4)> Math.operation({:square, 2})
invoking {:square, 2}
4

Еще одна альтернатива - сделать вашу функцию общедоступной, но указать с помощью @doc false, что они являются внутренними, т.е. не предназначены для публичного использования клиентами. Вы также можете рассмотреть возможность перемещения таких функций в отдельный модуль и пометить весь модуль @moduledoc false как внутренний. Оба подхода иногда используются в коде Elixir.

Однако я бы предложил начать простое и использовать функции сопоставления шаблонов + multi-clause. Если код становится более сложным, я бы рассмотрел другие варианты.

Ответ 3

Использование макросов

Вы можете использовать Macros для динамического вызова частных методов в том же модуле. Вот простой макрос, который выполняет это:

defmacro local_apply(method, args) when is_atom(method) do
  quote do
    unquote(method)(unquote_splicing(args))
  end
end

Чтобы вызвать его в своем модуле, вы можете сделать это (просто не забудьте определить макрос перед вызовом!):

def call_priv(some_argument) do
  local_apply(:something_private, [some_argument])
end

defp something_private(arg), do: arg

local_apply расширяется до нужного вам вызова метода с аргументами при вызове - но только во время компиляции - это означает, что вы не можете динамически расширять макрос до вызовов вашей функции во время выполнения.