1   Introduction

This post gives a few hints and suggestions about how to generate code from

This post makes no attempt to provide anything like complete documentation on Elixir meta-programming. For that, you can look at the following:

And, I found helpful hints in the following:

About ASTs (also called an Elixir quoted expression) -- In order to understand this post, you will need to understand what an Elixir AST is, how they are produced, what they are used for, and how they are used. To learn about Elixir AST, see https://elixir-lang.org/getting-started/meta/quote-and-unquote.html. You can also look at the documentation from within IEx. For example:

iex> h quote
iex> h unquote
iex> h Macro.to_string
iex> h Macro.expand

Also, look at the documentation for other functions in modules Macro and Code and Kernel.SpecialForms.

2   Sample code

The sample Elixir code for this mix application are here: elixir-meta-programming.zip.

3   Creating, building, and using a project

I used mix to create a project named my_mix_app as follows:

$ mix new my_mix_app

Build the project with the following:

$ cd my_mix_app    # or whatever your app is named
$ mix deps.get
$ mix deps.compile
$ mix compile
$ mix escript.build

Start the interactive Elixir interpreter and make our project available:

$ iex -S mix

Each time we edit and change a file, we can recompile at the interactive prompt with one of the following:

iex> IEx.Helpers.recompile
iex> recompile         # a shorter name for the same function

4   Debugging

While you are developing your Elixir meta-programming code generator, you will want to view your generated code. Here are a few suggestions and techniques for doing that.

Whenever you have generated an AST, which is what it means to generate code in Elixir, you can use the following to view it:

IO.inspect(ast, label: "ast")
IO.inspect(Macro.to_string(ast), label: "code")

5   Notes and details

5.1   Reading the specification file at compile-time

I encoded that specification of the functions in a Yaml file (see: https://yaml.org/). In retrospect, that was a questionable decision. The reason is that the Erlang/Elixir Yaml parser needs to load a NIF (a native implemented function, one that is implemented in C) and that NIF does not seem to get loaded early enough to be available when Elixir/IEx is started and out-of-date files are compiled. Okie-dokie, trying to turn this into a learning experience -- I wrote a function to read and parse the Yaml file and then save the resulting data structure as a string in a file.

You can read the Yaml file with something like the following:

{:ok, [spec | _]} = :fast_yaml.decode_from_file(in_spec_file)

And, you can write it out with this:

spec_str = Kernel.inspect(spec)
File.write("data_outfile.exs", spec_str)

See function save_yaml_spec_to_text_file/2 in the sample code.

Then, at compile-time, you can read in that Elixir data structure and save it in a module attribute (@methods in this case) with something like the following:

{:ok, spec_txt} = File.read("Data/module03.exs")
{spec, _} = Code.eval_string(spec_txt)
@methods :proplists.get_value("instance_methods", spec)

5.2   Generating a table at compile-time

I generated a dispatch table as an Elixir keyword list. In this case, a dispatch table is not really needed, since the user is passing in the name of the requested function as an atom. But, it's worth doing for demonstration purposes.

The dispatch table is generated be the macro ModuleGeneratorMacros.gen_dispatch_table:

defmacro gen_dispatch_table() do
  methods = ModuleGeneratorHelpers.get_methods()
  ast = for item <- methods do
    quote bind_quoted: [method_name: String.to_atom(:proplists.get_value("method_name", item))] do
      {method_name, method_name}
    end
  end
  #IO.inspect(ast, label: "ast")
  ast
end

Notes:

  • Notice the use of the Elixir list comprehension wrapped around quote/2. That produces a list of ASTs, which results in a list containing the items in a keyword list. In effect, we are injecting those ASTs into our Module. That's what happens when we call a macro.
  • And, we need to inject the value of the method name (as an atom) twice. So, rather than evaluate String.to_atom(:proplists.get_value("method_name", item)) twice, we use the bind_quoted option to the quote macro.

5.3   Generating functions at compile time

Normally (if you can consider meta-programming normal at all) we'd define a macro (with defmacro) to produce and insert an AST into our module. But, in this case, what we want to define are functions defined with the def expression. However, def itself is defined as a macro. And, that means that it returns an AST all by itself. And, that's what we want to insert, an AST.

So, all we need to do is repeatedly, in a loop, create def expressions. Here is some code that does that:

ModuleGeneratorHelpers.get_methods()
|> Enum.each(fn (item) ->
  method_name = :proplists.get_value("method_name", item)
  method_name_atom = String.to_atom(method_name)
  method_body_str = :proplists.get_value("method_body", item)
  {:ok, method_body_ast} = Code.string_to_quoted(method_body_str)
  # IO.inspect(method_body_ast, label: "method_body_ast")
  def unquote(method_name_atom)(args, opts) do
    unquote(method_body_ast)
  end
end)

Notes:

  • For each method or function, we capture the function name as an atom and the function body as a string.
  • In order to insert the name and the body, we unquote them. If we did not do so, we'd be inserting the variable names ("method_name_atom" and "method_body_str") rather than their values.
  • The call to IO.inspect/2, which is commented out, is for debugging purposes.

6   Conclusions and wrap-up

We've seen above, how we can generate elixir code from a specification. In this case that specification was represented in a Yaml file. As always there are annoyances and limitations. What are they?

For one, viewing the generated code is awkward and inconvenient. It's the reason you see things like the following scattered in the sample code (often commented out):

IO.inspect(ast, label: "ast")
IO.inspect(Macro.to_string(ast), label: "code")

The first line above prints out the "raw" AST of some generated code. The second line prints out the Elixir code represented by that AST.

And second, if you want to generate code and later modify it manually, using your text editor, ... well, I don't know how to do that. I think that the closest I could come to enabling that is to (1) produce the AST, then turn the AST into code (with Macro.to_string/1, and finally, write that string (the source code) to a file where I could edit it. That's not a very reasonable way of working, it seems to me. If that is indeed what you hope to be doing, then you'd likely be much happier, using any language of your choice, reading a specification file and then writing out source code to a file. If that is what you hope to accomplish, then perhaps Elixir meta-programming is the wrong technology for your task, and to use it would be a mis-use of Elixir meta-programming.

So, what would be a good use case for Elixir meta-programming? I'd say that you should look for these characteristics: (1) The data from which you want to generate the code is simple and voluminous. (2) The code to be generated from each data item is reasonably simple. And, (3) you do not have a need to edit or modify the generated code after it has been produced.


Published

Category

elixir

Tags

Contact