Hi.
I am making a library for user submitted templates (so users can customize email content). I have a parser which parses the template to an AST, and now I am implementing evaluating the template. My first pass was to interpret the template, going over the AST every time it renders, then my second pass was to build an anonymous function from the AST (so it is recursive anonymous functions for the different branches of the AST), now on my 3rd pass, I am building up an Elixir AST which in turn evaluates to a function.
My code uses quoted expressions, but it is easier to show the problem using strings:
with this string:
{compiledB, _} =
Code.eval_string("""
fn vars ->
%{"my_var" => userVar_0} = vars
[["(", to_string(userVar_0), ":", to_string(userVar_0), ")"]]
end
""")
Benchmark.measure(fn ->
%{"my_var" => 20}
|> compiledB.()
|> IO.iodata_to_binary
end)
Where Benchmark.measure calls the function 1000 times takes 0.025493 seconds, which is slower then interpreting it.
If I change it to do this:
Code.compile_string("""
defmodule A do
def render(vars) do
%{"my_var" => userVar_0} = vars
[["(", to_string(userVar_0), ":", to_string(userVar_0), ")"]]
end
end
""")
Benchmark.measure(fn ->
%{"my_var" => 20}
|> A.render()
|> IO.iodata_to_binary
end)
Running this 1000 times takes 4.37e-4 seconds
I like eval_string, since it returns an anonymous function right away and the function is isolated from the rest of the execution environment. Using the compile option, I can’t compile it into an anonymous function, only a module, which then gets put into the Elixir environment so I would have to make sure Module names are unique (but not too unique so I don’t run out of atoms). I would also need to purge manually any templates that aren’t used anymore, whereas with eval_string, I assume the garbage collector would handle cleaning them up automatically.
Why is eval_string several orders of magnitude slower? Is using compile_string and then keeping track of module names and purging them when no longer needed the way to go?
Thanks!