Get function name, module and arity from function capture

Hello!

I want to “save function” (module, name, arity) to the database to be able to call it later, potentially after BEAM was restarted. My plan is to use Functions.capture(module, function_name, arity) to be able to call the function after “loading” from the database.

How can I obtain module, function name and arity from a function capture (e.g. &MyModule.my_function/0) to “save function”? I have found Functions.info(fun) function, however, the documentation has a note saying that it should be used for debug only. What are the reasons it is debug only? What are the alternatives?

Not sure I follow. You have the module, the function name and the arity beforehand, why use the result of Function.capture result at all? Why not cut out the middleman and just store those 3 pieces in the DB? :thinking:

Not sure if I understood you correctly, but here is the clarification. I plan to use Functions.capture to “load function” from the database. My problem is how to “store function” to the database from the Elixir code, i.e. I want to write a function function_to_string(f) which can be called like this function_to_string(&MyModule.my_function/0) and would return [module: "MyModule", name: "my_function", arity: 0]" and this result I would store in the database.

So to answer your question - I agree I just need to store these 3 pieces in the DB, however, how to obtain them from a function capture.

Hey @stjefim, are you aware of Kernel.apply/3?

1 Like

I don’t think there is a general solution as function can be anonymous. Also, storing function in a db may not be the best idea. Code will change, db record last for a long time. Also, think of the security implication.

1 Like

Thank you, it will be useful after “loading function” - no need to do Functions.capture.

1 Like

You want to store the function’s bytecode itself in the DB? :thinking:

I still don’t get your requirement.

I agree with all your points:

  • I will not support anonymous functions.
  • Haven’t thought about versioning yet, but not a big problem for me.
  • Agreed - but can do some validation to allow only certain functions to be called.

No, I will not store byte code in DB. My requirement is to convert function capture, e.g. &MyModule.my_function/0 into module name as string, function name as string and function arity as integer.

That’s not possible. Because you can’t turn a function value back to MFA, just like you can’t turn a value back to the variable name.

You can store the function value itself though:

binary = :erlang.term_to_binary(&MyModule.my_function/0)
# store in db
fun = :erlang.binary_to_term(binary)
fun.()

However, as I mentioned above, that’s a big foot gun.

1 Like

The underlying Erlang function docs says it’s “mainly” for debugging:

This BIF is mainly intended for debugging, but it can sometimes be useful in library functions that need to verify, for example, the arity of a fun.

That said, the :telemetry library uses it to verify you supply an external function as a handler.

2 Likes

The specific thing you’re looking for is :erlang.fun_info:

iex(1)> f = &String.capitalize/1
&String.capitalize/1

iex(2)> :erlang.fun_info(f)
[module: String, name: :capitalize, arity: 1, env: [], type: :external]

Note that it gives a possibly-unexpected result if given a capture that uses &n forms:

iex(3)> f2 = &String.starts_with?(&1, "foo")
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(4)> :erlang.fun_info(f2)
[
  pid: #PID<0.0.0>,
  module: :erl_eval,
  new_index: 42,
  new_uniq: <<74, 179, 14, 23, 76, 2, 152, 184, 122, 207, 206, 42, 63, 68, 21,
    64>>,
  index: 42,
  uniq: 39164016,
  name: :"-expr/6-fun-3-",
  arity: 1,
  env: [
    {3, %{}, {:value, &:elixir.eval_local_handler/2},
     {:value, &:elixir.eval_external_handler/3}, %{},
     [
       {:clause, 3, [{:var, 3, :_capture@1}], [],
        [
          {:call, 3,
           {:remote, 3, {:atom, 3, String}, {:atom, 3, :starts_with?}},
           [
             {:var, 3, :_capture@1},
             {:bin, 3,
              [{:bin_element, 3, {:string, 3, ~c"foo"}, :default, :default}]}
           ]}
        ]}
     ]}
  ],
  type: :local
]
4 Likes