Defining an anonymous function of dynamic arity (not variadic)

I’m attempting to walk through Software Design for Flexibility and convert the examples to Elixir where possible. In doing so I’ve run into a challenge related to function arity.

defmodule Combine do
  def spread_combine(h, f, g) do
    n = arity(f)

    the_combination = fn args ->
      {fargs, gargs} = Enum.split(args, n)
      h.(apply(f, fargs), apply(g, gargs))
    end

    the_combination
  end

  defp arity(f) do
    :erlang.fun_info(f)[:arity]
  end
end

With this code, I can call spread_combine/3 as follows.

h = fn x, y -> ["h", x, y] end
f = fn x, y, z -> ["f", x, y, z] end
g = fn x, y, z -> ["g", x, y, z] end

Combine.spread_combine(h, f, g).([1, 2, 3, 4, 5, 6])
# > ["h", ["f", 1, 2, 3], ["g", 4, 5, 6]]

This works as expected. However, for a proper implementation of the code in the book, and I believe for optimal composability, I really want the function returned by spread_combine/3 to take the number of arguments n * 2 where n is the arity of f, as opposed to taking a single list of all args.

Is this possible with Elixir? I understand that variadic functions aren’t possible in Elixir. However, I’m not actually trying to create a variadic anonymous function here. In theory I should be able to define the anonymous function the_combination using the known arity of f multiplied by 2. I believe I can technically do that in a macro, using unquote_splicing and some lack of hygiene to still have access to all args for splitting between f and g perhaps, but I’m struggling.

If anyone more well versed in metaprogramming has any pointers I would appreciate the help. Thank you.

1 Like

Here’s a version that is close to what I want. I’m not sure how to get the arity of f in the macro version though.

defmodule Combine do
  defmacro spread_combine(h, f, g) do
    # How do I get the arity of f from the AST?
    # I'm hard coding it currently.
    f_arity = 2
    args = Macro.generate_arguments(f_arity * 2, __CALLER__.module)

    quote do
      the_combination = fn unquote_splicing(var!(args)) ->
        {fargs, gargs} = Enum.split(unquote(args), unquote(f_arity))

        unquote(h).(apply(unquote(f), fargs), apply(unquote(g), gargs))
      end

      the_combination
    end
  end

  # This doesn't work when f is AST
  def arity(f) do
    :erlang.fun_info(f)[:arity]
  end
end

This hardcoded arity version can be exercised with the following.

require Combine

h = fn x, y -> ["h", x, y] end
f = fn x, y -> ["f", x, y] end
g = fn x, y -> ["g", x, y] end

Combine.spread_combine(h, f, g).(1, 2, 3, 4)
1 Like

I’m not certain it’s possible to do that - for instance, in the example shown the actual value of f passed to the macro is something like {:f, [line: NNN], nil}. The arity isn’t fully knowable until runtime, especially since functions can appear from the void with :erlang.binary_to_term/1:

# on one machine
binary_data = :erlang.term_to_binary(fn x, y -> IO.inspect(x, label: y) end)

# send binary_data over the network to another node, store it on disk, etc
# on the other machine
f = :erlang.binary_to_term(binary_data)
# f is now an arity-2 anonymous function
2 Likes

Interesting exercise, and a tricky one to implement.
It would be easily solvable, but you cannot use unquote_splicing to pass arguments to special forms (If I understand correctly the issue). So you need to build by AST by hand.
Here’s a solution
Build anonymous functions with a variable length of arguments · GitHub

defmodule Variadic do
  @moduledoc """
  Solution for https://elixirforum.com/t/defining-an-anonymous-function-of-dynamic-arity-not-variadic/38228
  """

  defmacro spread_combine(h, f, g) do
    quote bind_quoted: [h: h, f: f, g: g, module: __CALLER__.module],
          location: :keep do
      {:arity, f_arity} = Function.info(f, :arity)
      {:arity, g_arity} = Function.info(f, :arity)
      args = Macro.generate_arguments(f_arity + g_arity, module)
      {f_args, g_args} = Enum.split(args, f_arity)

      # The trick here is in order to bypass the limitation of using unquote_splicing to pass arguments to special forms,
      # we build the AST "by hand" and evaluate it.
      Variadic.deffn args do
        h.(apply(f, f_args), apply(g, g_args))
      end
    end
  end

  @doc false
  # Builds the Kernel.SpecialForms.fn/1 AST
  def deffn(args, do: body) do
    fn_ast =
      {:fn, [],
       [
         {:->, [], [args, body]}
       ]}

    Code.eval_quoted(fn_ast) |> elem(0)
  end
end
2 Likes

Thank you @eksperimental! I would not have guess this one.

I like how your code doesn’t assume f and g have the same number of args either. I also forgot that Elixir had Function.info wrapping up :erlang.fun_info.

Note that there’s a typo with the second Function.info call passing f instead of g.

@al2o3cr I forget to keep in mind the dynamic nature of how even functions could be deserialized and invoked. I need to think more to fully bake in my mind the implications. Thanks for the response.

1 Like

You are welcome!

That was a last minute change. I have fixed it in the gist, but I think since my post was chosen as the answer it does not allow me to update it.

I use Code.eval_quoted/3 for a more dynamic behavior:

  def compose(f, g) do
    {:arity, arity} = Function.info(g, :arity)

    args = for _ <- 1..arity, do: Macro.unique_var(:arg, __MODULE__)

    fn_ast =
      quote do
        fn unquote_splicing(args) ->
          var!(f).(var!(g).(unquote_splicing(args)))
        end
      end

    {compose_fn, _} = Code.eval_quoted(fn_ast, f: f, g: g)

    compose_fn
  end
2 Likes

I tweaked it to make it work

I like your solution better because it is a function, not a macro.
Very interesting. Thank you @dsdshcym

I’m a little late to this, but this is what I did when I needed to make an anonymous function with a dynamic arity:

 defp make_lambda(arity, fun, args, type) do
    arg_list = for arg <- 1..arity do
      last_arg = if arg != arity do ", " else "" end
      "arg" <> to_string(arg) <> last_arg
    end

    arg_list = List.to_string(arg_list)
    fun_cmd = "fn(" <> arg_list <> ") -> Curry.do_generate_next(fun, args ++ [" <> arg_list <> "], type) end"

    {lambda, _} = Code.eval_string(fun_cmd, fun: fun, args: args, type: type)

    lambda
  end

See: https://github.com/nhpip/curry-elixir