tmbb

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.

First 10 of 17 Posts Switch mode

mayel

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 quote and CodeGen.block "f1/1" do which doesn’t feel very idiomatic. For simplicity and ease of code reuse, have you considered defining the template module with regular functions, eg:

defmodule CodeGenTemplate do
  @c1 1

  def f1(x, c1 \\ @c1) do
    x + c1
  end
end

So then use CodeGen can take care of adding defdelegate to your module for each function in the template that isn’t also defined in your module, and the show_blocks and dump_source functions could do something like this to read/copy the source:

[edit] full code here: module_extend.ex · GitHub

tmbb

tmbb OP

quote/unquote is 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 a Bootstrap5Components, 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

mayel

Oh and here’s some more code I had laying around experimenting with the defdelegate part of this. Plus it defines defoverridable which means you can use super :slight_smile:

[edit] full code here: module_extend.ex · GitHub

mayel

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:

defmodule CodeGenTemplate do
  @c1 1

  def f1(x, c1 \\ @c1) do
    x + c1
  end
end
tmbb

tmbb OP

If I remember correctly, the advice against config options was exactly against setting options using config.exs/Application.put_env/3 because 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

mayel

In your example, isn’t that what Phoenix.Component’s attr is for? eg. <.input width=9 /> so you can change the options when using a component?

mayel

mayel

No worries, it’s nice to see someone else playing with these kind of things as well!

tmbb

tmbb OP

Not exactly. I want to be able to generate the following AST:

attr :label_width, unquote(default_label_width)
attr :input_width, unquote(default_input_width)

def input(assigns) do
  # ...
end

So that I can do something like this:

MyAppWeb.CoreComponents do
  use CodeGen,
    module: Bootstrap5Components,
    options: [
      default_label_width: 3,
      default_input_width: 9
    ]
end

but such that in other project (or the same project) I can have components with different parameters.

mayel

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 inject_before_final_end function from CodeGen (thanks @tmbb :slight_smile: how are you planning the library?) we can inject/dump a function from a template module into our code.

[edit] full code here: module_extend.ex · GitHub

D4no0

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.

Where Next? Top

Trending in Announcing Top

bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
387 15136 120
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 11030 135
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
woylie
Phoenix components for pagination, sortable tables and filter forms with Flop and (optionally) Ecto. pagination cursor pagination sorta...
New
kip
Please say hi to a new lib, Astro that aims to deliver easy-to-consume astronomy calculations of practical use. For now it only calculat...
New

Other Trending Topics Top

akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
bartblast
Hey folks, I just published a post about Hologram’s funding and where the project goes next - the short version: Curiosum as Main Spons...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

We're in Beta

About us Mission Statement