Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elixir - Call private function dynamically

Tags:

elixir

I've found Kernel.apply/3 which allows dynamically calling a public method in a module by specifying the method as an atom, e.g. result = apply(__MODULE__, :my_method, [arg]) translates to result = my_method(arg)

What baffles me is a way to call a private method; Given code like this:

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

I would expect that MyModule.do_priv(:something_private, [1]) to be permissible, since it's a call to a private method from within the module. I can appreciate that under the hood Elixir is using Erlang's apply/3, and so this approach probably isn't going to get us there.

I've also tried using the Code.eval_quoted/3 method, but it doesn't even seem to be capable of calling a hardcoded private method (and hence no time spent building the AST by hand, rather than using quote do as below- though that's an option if someone sees how to make this work):

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

Again, it's access to a private function from within the containing module, so I would expect it to be permissible. Its possible that I just don't understand the __ENV__ parameter to eval_quoted

The only working solution right now is changing defp to def, which is a fine solution for my personal code; but since I write code that supports other programmers who do care, I'd like to find a solution.

I'm open to other approaches, but I'm personally stumped on how to make this happen.

like image 817
Chris Meyer Avatar asked Mar 08 '15 17:03

Chris Meyer


3 Answers

At first, you should know that f() called inside a MyModule module is not the same thing as MyModule.f() called in the same place. See http://www.erlang.org/doc/reference_manual/code_loading.html#id86422

You can call private functions only f() style. These calls are also checked by the compiler - if the function does not exist, you get compile error. When you use MyModule.f() in the same place, you do not get compile error because these calls are checked at runtime only (even if you are calling the module from within itself) and the effect is (AFAIK) the same as if you were calling MyModule.f() from any other module - the module is looked up in runtime and you can call only exported (public) functions.

Therefore you can not call private functions in any other means than just a plain f(). apply(mod,fun,[]) is an equivalent to mod.fun.() style - the module is resolved at runtime and private functions are not accessible.

You can try all the variants by yourself in this example: https://gist.github.com/mprymek/3302ff9d13fb014b921b

You can see now that calls to private functions must always be known at the compilation time, so you can't use even eval_quoted magic or any other magic to make them "dynamic"...

Sasa Juric's advice to use @doc false is the right solution.

like image 166
Miroslav Prymek Avatar answered Nov 19 '22 01:11

Miroslav Prymek


AFAIK dynamically invoking private functions is not possible in Erlang (and therefore not in Elixir). If you need to do a dynamic dispatch, you could consider using a multi-clause function. A contrived example (surely a bad one, but can't think of a better ATM):

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

Another alternative is to make your function public, but indicate with @doc false that they're internal - i.e. not meant to be used publicly by clients. You can also consider moving such functions to a separate module, and mark the whole module with @moduledoc false as internal. Both approaches are occasionally used in Elixir code.

I would however suggest starting simple, and use pattern matching + multi-clause functions. If the code becomes more complex, I would then consider other options.

like image 21
sasajuric Avatar answered Nov 19 '22 02:11

sasajuric


Using Macros

You can use Macros to dynamically call private methods within the same Module. Here's a simple macro that accomplishes that:

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

To call it in your module you can do this (Just remember to define the macro before calling it!):

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

defp something_private(arg), do: arg

local_apply would expand to your desired method call with arguments when called - but only at compile time - that means you can't dynamically expand the macro to your function calls at runtime.

like image 5
Sheharyar Avatar answered Nov 19 '22 01:11

Sheharyar