Recursion is the core of Elixir and Erlang code. But it is missing from anonymous functions.
iex(9)> sum = fn [] -> 0; [h|t] -> h + sum.(t) end
** (CompileError) iex:9: undefined function sum/0
(stdlib) lists.erl:1353: :lists.mapfoldl/3
(stdlib) lists.erl:1354: :lists.mapfoldl/3
Why? In a match expression the right hand side is evaluated first, then matched to the left.
So the variable sum has not yet been assigned a value when fn [] -> 0; [h|t] -> h + sum.(t) end is evaluated.
Recursive anonymous functions were added to Erlang in R17. Elixir solved the match problem by including a name in the right hand side of the match expression.
From Joe Armstrong's Announcement:
F = fun Fact(0) -> 1;
Fact(N) -> N * Fact(N - 1)
end.But elixir core team has not yet finalized if or how they will include the same functionality.
But there's no need to wait; using macros we can extend elixir syntax (almost) however we'd like.
Mathematicians and computer scientists have solved the problem of implementing recursion: the fixed-point combinator
I am not a mathematician or a computer scientist but thanks to The Little Schemer I know a bit about combinators anyway. And fortunatly others have already written some in elixir. Here is a Y combinator, and here is a Z combinator
I will use the z combinator from exyz as a template. exyz is available on hex. It looks like this:
def z_combinator f do
combinator = fn(x) ->
f.(fn(y) -> x.(x).(y) end)
end
combinator.(combinator)
endAnd works like this:
factorial = Exyz.z_combinator fn(f) ->
fn
(1) -> 1
(n) -> n * f.(n - 1)
end
end
factorial.(5) == 120Beautiful. But it is limited: it can only handle functions with an arity of 1.
(This is not really a limitation. Using a list or tuple argument is natural and easy. But I wanted to try and improve it anyway)
I want to build a macro that can handle all functions, regardless of arity. This is the syntax I want:
f = rfn count, fn
(_, [], c) -> c
(x, [x|t], c) -> count.(x, t, c + 1)
(x, [_|t], c) -> count.(x, t, c)
end
f.(:a, [:a, :b, :b, :a], 0) == 2I will be building off of the Z combinator in exyz:
def z_combinator f do
combinator = fn(x) ->
f.(fn(y) -> x.(x).(y) end)
end
combinator.(combinator)
endAfter staring at it for a half hour I determined I'd need to change all occurrences
of y to y0, y1 ... yN where N == arity(f)
Lets take a look at the abstract syntax tree of an fn
iex> quote do fn(a, b) -> :ok end end
{:fn, [], [{:->, [], [[{:a, [], Elixir}, {:b, [], Elixir}], :ok]}]}I can see where args a and b are. So I'll need 3 functions. On to count the number of
args in a fn, one to generate n args, and one to create an abstract syntax tree with those args.
Generating args is simple. We can use Macro.var/2
def gen_args(0, args), do: args
def gen_args(n, args) do
n_args(n - 1, [Macro.var(:"arg_#{n - 1}", __MODULE__) | args])
endTesting it out:
iex> args = gen_args(2, [])
[{:arg0, [], Rfn}, {:arg1, [], Rfn}]
iex> ast = quote do fn unquote(args) -> :ok end end
{:fn, [], [{:->, [], [[[{:arg0, [], Rfn}, {:arg1, [], Rfn}]], :ok]}]}
iex> Macro.to_string(ast)
"fn [arg0, arg1] -> :ok end"That didn't quite work. Instead of a fn with arity 2, I generated a function with
arity 1 that matched against a 2 element list.
So here's where I get a bit tricky. While I would never use this in production, nothing is preventing me from hand-rolling an abstract syntax tree.
defp fn_ast(vars, meta, body) do
{:fn, meta, [{:->, meta, [vars, body]}]}
endTesting it out:
iex> args = gen_args(2, [])
[{:arg0, [], Rfn}, {:arg1, [], Rfn}]
iex> ast = fn_ast(args, [], :ok)
{:fn, [], [{:->, [], [[{:arg0, [], Rfn}, {:arg1, [], Rfn}], :ok]}]}
iex> Macro.to_string(ast)
"fn arg0, arg1 -> :ok end"Much better.
Finally a function to count the number of args. Reverse the fn_ast code above and tweak it a bit.
voilà
defp num_args({:->, _, [args | _body]}) do
length(args)
endNow we have a way to genereate a fn with arity n we can improve the z combinator from before to handle fns of any arity!
defmacro rfn(var, {:fn, meta, [c|_clauses]} = f) do
n = num_args(c)
args = gen_args(n, [])
namedf = quote do
fn (unquote(var)) -> unquote(f) end
end
combinator_fun = combinator_ast(namedf, args, meta)
quote do
combinator = unquote(combinator_fun)
combinator.(combinator)
end
end
defp combinator_ast(namedf, args, meta) do
xvar = Macro.var(:x, __MODULE__)
# fn (args...) -> xvar.(xvar).(args...) end
inner = fn_ast(args, {
{ :., meta,
[ { {:., meta, [xvar]}, meta,
[xvar] } ] },
meta, args }, meta )
# fn (xvar) -> var.(inner) end
quote do
fn unquote(xvar) -> unquote(namedf).(unquote(inner)) end
end
endtesting it out:
iex> import Rfn
nil
iex> f = rfn count, fn
...> (_, [], c) -> c
...> (x, [x|t], c) -> count.(x, t, c + 1)
...> (x, [_|t], c) -> count.(x, t, c)
...> end
#Function<18.54118792/3 in :erl_eval.expr/5>
iex> f.(:a, [:a, :b, :b, :a], 0) == 2
trueIt works!
Macros are hard. Even harder is manipulation of the elixir abstract syntax tree. There is no guarantee the ast will not change in different environments and different platforms. I already know many situations where the code given here will fail. For example it does not handle guard clauses.
I've already found and fixed a few limitations not covered in the code given above. Look at the source on github for more details and some working code. But I won't be publishing this to hex because I don't want anyone using it for anything important.
Feel free to hit me up with questions, comments or ways the code could be improved. In particular I'm wondering if there is a way to achieve what fn_ast does using quote and unquote.