tmbb
CodeGen
(experimental!) code here: GitHub - tmbb/ex_code_gen: Flexible code generation for Elixir · GitHub
Suppose you want to have your library generate some code in the user’s module. That is pretty common in Elixir, and it’s usually done through the use XXX macro.That is flexible and succint, in that it doesn’t add any source code to the user’s file. On the other extreme, you can have code generators (normally called using mix package.gen.something ...). These generators dump literal source code in the middle of your project, often adding hundreds of lines but allow you to customize those lines as much as you want.
However, up until now, there isn’t a simple way of combining both approaches above. Ideally, one would use a module but retain the ability to add the literal source code of several functions so that they can be customized. This (experimental!) package was prompted by the discussion here: https://forum.elixirforum.com/t/phoenix-1-7-feels-a-little-bit-locked-in-with-tailwindcss/54468/43
I’m tagging @josevalim and @thiagomajesk because of that discussion.
Let’s look at an example. First, let’s define a code template, which is simply a module that defines a __code_gen__(options) function (not a macro, there’s no need for it to be a macro!) which returns the AST of the code we want to inject into the module (the code is large, but it’s mainly because it’s highly commented, the actual injected code is very simple):
defmodule CodeGenTemplate do
def __code_gen__(opts) do
c1 = Keyword.get(opts, :c1, 1)
c2 = Keyword.get(opts, :c3, 2)
c3 = Keyword.get(opts, :c3, 3)
quote do
# Define some code inside a block, which we'll be able
# to dump into our own module if we want
CodeGen.block "f1/1" do
@constant1 unquote(c1)
def f1(x) do
x + @constant1
end
end
# Anothe code block
CodeGen.block "f2/1" do
def f2(x), do: x + unquote(c2)
end
# This code block is more complex and has comments
CodeGen.block "f3/1" do
# Comments are completely removed by Elixir's parser.
# Ways of preserving comments while easily allowing AST
# manipulation in an idiomatic way.
#
# The workaround is to use these custom attributes
# which are completely removed from the AST and replaced
# by comments when you dump the source code in the module
@comment__ "This is a comment outside the function"
@comment__ "This is a another comment outside the function"
@comment__ "Yet another comment"
@newline_after_comment__
def f3(x) do
@comment__ "this is a comment inside the function"
x + unquote(c3)
end
end
# You can define functions outside a code block
# if you don't want the user to be able to redefine the function.
def another_function() do
# ...
end
# We need to mark some functions as overridable so that we can actually
# dump their source code into the module and things will work.
# It's too hard for CodeGen to understand which functions are being generated
# and generate this list on its own.
defoverridable f1: 1, f2: 1, f3: 1
end
end
end
Now, we can use that code in a module:
defmodule CodeGenExample do
use CodeGen,
# The `:module` is our template, which must define a `__code_gen__(options)` function
module: CodeGenTemplate,
# Use the options to customize the generated code
options: [
c1: 7
]
end
Looking at the example module, we see that the code above is quite similar to a normal use CodeGenTemplate invocation, except for the fact that it is more explicit regrading the fact that we’re using the CodeGen module, which will support additional features.
Now, suppose we want to customize the f1/1 function. The way to do it is to edit the source code directly, but the problem with macros that generate code is that there is no source code for us to edit! However, this is where the special features in the CodeGen module become useful. Remember we have defined a number of named blocks. First, we can query the module to see which block names are available (of course, the author of the template module should make that clear in the documentation, but querying the block names is always halpful):
iex> CodeGen.block_names(CodeGenExample)
["f1/1", "f2/1", "f3/1"]
Nice! But we’d like to be able to actually see the blocks’ contents, so that we know what we’ll be including in advance. That is also easy:
iex(6)> CodeGen.show_blocks(CodeGenExample)
┌────────────────────────────────────────────────────────
│ Block: f1/1
├───────────────────────────────────────────────────────
│ @constant1 7
│ def f1(x) do
│ x + @constant1
│ end
└───────────────────────────────────────────────────────
┌────────────────────────────────────────────────────────
│ Block: f2/1
├───────────────────────────────────────────────────────
│ def f2(x) do
│ x + 2
│ end
└───────────────────────────────────────────────────────
┌────────────────────────────────────────────────────────
│ Block: f3/1
├───────────────────────────────────────────────────────
│ # This is a comment outside the function
│ # This is a another comment outside the function
│ # Yet another comment
│
│ def f3(x) do
│ # this is a comment inside the function
│ x + 3
│ end
└───────────────────────────────────────────────────────
:ok
Now we know which code there is in each block. Suppose we want to dump the contents of block f1/1 into our own file. We just need to do:
iex> CodeGen.dump_source(CodeGenExample, "f1/1")
* injecting test/fixtures/immutable/code_gen_example.ex
:ok
The file contents have been replaced by:
defmodule CodeGenExample do
use CodeGen,
module: CodeGenTemplate,
options: [
c1: 7
]
@constant1 7
def f1(x) do
x + @constant1
end
end
You can now customize the f1/1 function at will, while not having your code polluted by the code of the other functions you don’t need.
Applications
This CodeGen module is useful in any situation where you want to put some code in a module without adding literal code to the file, but in which you think you might have to customize some of the functions by editing the source code.
I can think of some uses for this:
Library behaviours, such as GenServer
Let’s say that instead of writing use GenServer you could write use CodeGen, module: GenServer. That way the GenServer module could define all callbacks inside the module, but you could do things such as CodeGen.dumo_source(MyGenserver, "handle_info/2") to have skeleton implementtion which you can edit´
Phoenix CoreComponents
Phoenix CoreComponents are meant to be customized by the user. However, the truth is that the default Phoenix generators dump A LOT of source code into the default project, some of which the user doesn’t care about, at least at the beginning. Some people (myself included) have complained about it here: https://forum.elixirforum.com/t/phoenix-1-7-feels-a-little-bit-locked-in-with-tailwindcss/54468/43. One of the suggestions made by @josevalim is that people should publish CoreComponents files customized to certain CSS frameworks (such as Bulma, Bootstrap, etc.). With CodeGen, one can publish a package that provides a custom CoreComponents file, which can be used like this:
defmodule MyAppWeb.CoreComponents do
use CodeGen,
# The (foreign) module providing the core components
module: Bootstrap5CoreComponents,
# Module-level customizations
options: [
horizontal_forms_by_default?: true,
label_width: 3,
input_width: 9,
# ...
]
end
If the user wants to customize something like an input/1 component, then it’s simple to just start iex and do:
iex> CodeGen.dump_source(MyAppWeb.CoreComponents, "input/1")
* injecting test/fixtures/immutable/code_gen_example.ex
:ok
The code above could insert the code for the input/1 component, whitout polluting the source with other components which the user doesn’t need to modify.
Inclusion in Elixir’s Standard Library
I’ve looked into many languages which provide intersting facilities for code generation. The main ones are variants of Lisp in one way or another, but there are non-SExpr-based languages with such capabilities, such as OCaml (through a more or less complex build step), Haskell (which seems to provide very useful metaprogramming facilities, but which I’ve never tried due to the general dificulty of doing most things in Haskell as well as fear of lazy evaluation), Rust (which is kinda too low level for me). Other languages, like C have preprocessor-based metaprogramming.
Elixir has very impressive metaprogramming capabilities, which sets it apart from most other languages I’ve tried, and at the level of Lisp (and certainly more ergonomic than all the non-Lisp languages I’ve tried). However, the most unique feature of Elixir’s development I’ve found is the emphasis on generators to generate actual code which can be customized by the user. Phoenix’s generators, in particular, are a marvel in terms of generalizability and implementation (I know it because I’m maintaining parallel generators for my Mandarin admin package, and it’s actually quite hard to implement, maintain and test such generators).
I think that the functionality I’ve built with CodeGen bridges the functionality of metaprogrammning facilities (succint, safe, non-customizable) together ith the functionality of code generators (verbose, less safe - we never know what the user will do to “our” code, very customizable). This is an idea I’ve had for a long time but which up until now I’ve never got around to implement it. The implemenation is in a single (very short) file.
Given the emphasis given in Elixir to both metaprogramming and code generation, I’d like to encourage experimentation with these kinds of ideas (not necessarily with CodeGen, but with other similar tools one can develop) so that this could be stabilized and maybe included in the Elixir standard library.
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










First 10 of 17 Posts
mayel
I like where you’re going with this, and how you said the definition does not need a macro, but you go on to use
quoteandCodeGen.block "f1/1" dowhich doesn’t feel very idiomatic. For simplicity and ease of code reuse, have you considered defining the template module with regular functions, eg:So then
use CodeGencan take care of addingdefdelegateto your module for each function in the template that isn’t also defined in your module, and theshow_blocksanddump_sourcefunctions could do something like this to read/copy the source:[edit] full code here: module_extend.ex · GitHub
tmbb
quote/unquoteis still the most idiomatic way of generating dynamic AST. Your proposal of having a “raw module” doesn’t allow the template author to support dynamic generating an AST that depends on dynamic options. For example, if you want to provide aBootstrap5Components, you might want to allow the user to choose whether dorms are horizontal or vertical by default. How does your proposal deal with such dynamic options?mayel
Oh and here’s some more code I had laying around experimenting with the
defdelegatepart of this. Plus it defines defoverridable which means you can usesuper[edit] full code here: module_extend.ex · GitHub
mayel
That’s part of being more idiomatic too, see for example the official advice about not using config for libraries but instead passing options as function arguments. I think this code suggests a middle ground approach (so you could pass it as a function argument in any case, and redefine the default in your module if you’ve dumped the source:
tmbb
If I remember correctly, the advice against config options was exactly against setting options using
config.exs/Application.put_env/3because those are global in your application. Generating customized modules according to “config options” is completely different, because nothing is global: you simply define a module customized for your purposes, and different part of the application can generate different module. I believe that generating different code according to different options is an important feature, and the driver of most of my architecture.The idea is precisely not to have to pass optional arguments, or even to customize what the default value of the parameter is! This is precisely the case with my PhoenixComponents example. One may want to specify a default width for form fields (i.e., set a default value dynamically at compile-time) and allow the user to override it on a case-by-case example (by making it an optional parameter in the component).
I think we have pretty different opinions fundamental principles of code organization and on the proper role for dynamic AST generations, and that’s ok, but I can’t see how your (admittedly more elegant proposals) can support the dynamic AST generation I need.
mayel
In your example, isn’t that what
Phoenix.Component’sattris for? eg.<.input width=9 />so you can change the options when using a component?mayel
No worries, it’s nice to see someone else playing with these kind of things as well!
tmbb
Not exactly. I want to be able to generate the following AST:
So that I can do something like this:
but such that in other project (or the same project) I can have components with different parameters.
mayel
@tmbb makes sense, best of luck!
For anyone else following along, I’ve slightly updated the previously posted code (notably to obtain the last line number of a function definition along with the first so we can copy a entire function’s definition(s) block out of a code file), which means that combined with the
how are you planning the library?) we can inject/dump a function from a template module into our code.
inject_before_final_endfunction fromCodeGen(thanks @tmbb[edit] full code here: module_extend.ex · GitHub
D4no0
While your idea is not bad, since this can be achieved with functions only, I don’t think the complexity you are introducing is worth the result. The first rule of metaprogramming is to avoid the use of macros if the same result can be achieved with functions, especially when it comes to DSLs with new concepts.
I am with the same opinion as @mayel in regard of the syntax, if you really want to abstract function generation, you need to get rid of the macro syntax, otherwise I will just use classic metaprogramming as I already know how those concepts work and scale, I don’t have to think about the magic you are doing under the hood.
Having said all of this, this might be a good tool oriented not to end developer, but as a building block of a library that further can abstract implementation details.