Is it possible to programmatically construct a function capture without using `Code.eval_quoted`?

So while writing Confy, I wanted to turn shorthand atoms like:integer, :string, :atom into the ‘normal’ forms &Confy.Parsers.integer/1, &Confy.Parsers.string/1, &Confy.Parsers.atom/1, etc.

However, I struggled to properly perform quoting and unquoting while using the function capture operator&.

In the end, I settled on the following snippet:

  # Replaces simplified atom parsers with
  # an actual reference to the parser function in `Confy.Parsers`.
  # NOTE: I dislke the necessity of `Code.eval_quoted` here, but do not currently know of another way.
  defp normalize_parser(parser) when is_atom(parser) do
    case Confy.Parsers.__info__(:functions)[parser] do
      nil -> raise ArgumentError, "Parser shorthand `#{inspect(parser)}` was not recognized. Only atoms representing names of functions that live in `Confy.Parsers` are."
      1 ->
        {binding, []} = Code.eval_quoted(quote do &Confy.Parsers.unquote(parser)/1 end)
        binding
    end
  end
defp normalize_parser(other), do: other

Note the Code.eval_quoted line: Although it is probably safe since it is only executed when parser indeed is an atom and exists as a function inside the Confy.Parsers module, it does not feel nice to need it.
Is there another way to build a function capture out of a module and function atom?

Note that 'function captures like wrapping everything with fn are not what I am looking for, since the result of normalize_parser is also used to create documentation:
inspect(&Module.foo/1) is "&Module.foo/1" whereas
inspect(fn x -> Module.foo(x) end) is "#Function<6.127694169/1 in :erl_eval.expr/5>".

Thanks! :smile:

1 Like

Would not &apply(Confy.Parsers, parser, [&1]) work? :slight_smile:

apply/3 is so very useful. ^.^

iex(1)> h apply/3

                     def apply(module, function_name, args)                     

  @spec apply(module(), function_name :: atom(), [any()]) :: any()

Invokes the given function from module with the list of arguments args.

apply/3 is used to invoke functions where the module, function name or
arguments are defined dynamically at runtime. For this reason, you can't invoke
macros using apply/3, only functions.

Inlined by the compiler.

## Examples

    iex> apply(Enum, :reverse, [[1, 2, 3]])
    [3, 2, 1]

Consequently it has no speed hit over a normal indirect call either since they lower to the same beam code, just to be sure I’ve also benchmarked it. ^.^

2 Likes

Also instead of this, this is faster:

if function_exported?(Confy.Parsers, parser, 1) do
iex(2)> h function_exported?

                def function_exported?(module, function, arity)                 

  @spec function_exported?(module(), atom(), arity()) :: boolean()

Returns true if module is loaded and contains a public function with the given
arity, otherwise false.

Note that this function does not load the module in case it is not loaded.
Check Code.ensure_loaded/1 for more information.

Inlined by the compiler.

## Examples

    iex> function_exported?(Enum, :member?, 2)
    true
1 Like

Using function_exported? is definitely the way to go. Thank you, I did not know about that function!

As for using apply: It would end up in the documentation as well:
See f.e. here:

Validated/parsed by calling &Confy.Parsers.boolean/1 .

this would then become

Validated/parsed by calling &apply(Confy.Parsers, :boolean, [&1]) .

which is not nearly as nice.

Thus I really am looking for a way to construct &SomeModule.foo/1 where foo is a dynamic value.

If there is no way around sing Code.eval_quoted then that is fine, but I did want to check :slight_smile:.

Function.capture/3 is what you are looking for.

5 Likes

Wow, there’s so many little goodies in the stdlib. Never ceases to amaze me.

1 Like