Eiji

Eiji

Warning: Calling this function inside a macro is considered bad practice as it will attempt to evaluate runtime values at compile time. Macro arguments are typically transformed by unquoting them into the returned quoted expressions (instead of evaluated).

Source: Code.eval_quoted/3

I’m not sure if I understand it fully. Is it only about “runtime values” (like variables)? Does it apply also to do: block last argument for nested DSL?

For simplicity let’s assume that you start with a simplest DSL, but with 1 extra nested DSL. You prefer do … end way because it’s more flexible, so for example you can use a for … do … end special form to generate nested DSL calls based on some input. Consider below example:

defmodule MyLib do
  defmacro first_case_root(do: block) do
    quote do
      unquote(block)
    end
  end

  defmacro second_case_root(do: block) do
    Code.eval_quoted(block, [], __CALLER__)
  end

  defmacro nested do
    nil
  end
end

defmodule MyApp do
  import MyLib

  first_case_root do
    nested()
  end

  second_case_root do
    nested()
  end
end

In first case the nested macro is called when unquoting a result of the parent macro. For simplicity let’s say it’s called between first_case_root and second_case_root. This way looks a bit complicated as to store accumulated arguments we have to work in inner macro (inside quote) and/or on @before_compile.

However I found something obvious … I can just evaluate quoted block (as shown in second case). This way the nested DSL is evaluated inside a parent macro which in fact flattens nested DSL calls. This allows to work on the parent macro logic inside … a parent macro.

Now let’s go back to the quote. I understand why AST of stuff like variables should not be evaluated especially because in many macros it’s completely not important. However in this case “flattening” nested DSL calls allows to simplify code a lot:

  1. Even in simplest example it’s 1 LOC vs 3 LOC where one line have extra indention.

  2. There is no need to work on module attributes to store compile-time data. Instead we can use many if not all in-memory solutions. The simplest one is most probably Process.put/2

  3. Depending on use case the nested DSL macros does not have to return any quoted expressions and the root DSL macro returns a minimal quoted expression without storing and manipulating data

{nil, []} = Code.eval_quoted(block_with_nested_dsl_calls, [], __CALLER__)

Both macro code and the code returned by macro are much simpler. Simply imagine that a root macro with dozens or even more nested macro calls returns a minimal code which you print like tap(& &1 |> Macro.to_string() |> IO.puts()) and the output contains only desired generated code (generated functions) without lots of other lines you have to scroll in console.

Does described case is also seen as a bad practice? Or as written above the warning was intended only for “runtime values” like variables? If this is bad idea could you please explain why? Are there any side-effects of doing so?

Showing Posts 1 to 4

D4no0

D4no0

From what I understand calling this function will literally evaluate that AST at compile-time, imagine you have the following:

a = 10
b = 20

quoted = quote do a + b end

{:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]],
 [
   {:a, [], Elixir},
   {:b, [context: Elixir, imports: [{1, IEx.Helpers}]], Elixir}
 ]}

# Evaluating this will result in an error
Code.eval_quoted(quoted)

This will always result in an error, because the values don’t exist from the context you are trying to evaluate the expression (unless you define them manually in that context), hence composition of AST, that will be executed later by the compiler in the correct context is recommended instead of its evaluation at compile-time.

Eiji

Eiji OP

If you want to do so then you can use bindings parameter which defaults to []. In my case it’s not a problem.

defmodule MyLib do
  defmacro root_dsl(do: block) do
    Process.put(:nested_values, [])
    Code.eval_quoted(block, [], __CALLER__)
    values = Process.get(:nested_value)
    processed_values = Enum.map(values, &process/1)

    quote bind_quoted: [values: processed_values] do
      for value <- values do
        def some_function(unquote(value)), do: {:ok, value}
      end
    
      def some_function(value), do: {:error, "incorrect value: #{value}"}
    end
  end

  defmacro nested_dsl do
    value = # …
    values = Process.get(:nested_value)
    Process.put(:nested_values, [value | values])
    nil
  end
end

Look that there is no use of module attribute. Only current process at compilation time stores the values, most of the logic happens in root_dsl macro and the block in quote call is as simple as possible.

Without Code.eval_quoted/3 it would look like:

defmodule MyLib do
  defmacro root_dsl(do: block) do
    [
      quote do
        Process.put(:nested_values, [])
        unquote(block)
      end,
      quote unquote: false do
        values = Process.get(:nested_value)
        processed_values = Enum.map(values, &process/1)

        for value <- processed_values do
          def some_function(unquote(value)), do: {:ok, value}
        end
        
        def some_function(value), do: {:error, "incorrect value: #{value}"}
      end
    ]
  end

  defmacro nested_dsl do
    quote do
      value = # …
      values = Process.get(:nested_value)
      Process.put(:nested_values, [value | values])
    end
  end
end

Of course it’s just an example, but it shows really well how version with Code.eval_quoted/3 looks better. The question is if it’s anyway seen as a bad practice and if so why …

D4no0

D4no0

I mean that is just a generic warning, you can do whatever you want in your macros.

I personally would say that using Code.eval_quoted/3 inside of your macros is an imperative way of doing things and composing the AST is the declarative way, as you are composing a data structure that is later evaluated.

I don’t have experience doing this kind of shenanigans, however I would suspect you running into trouble with evaluation contexts sooner or later when you are doing this kind of intermediary evaluations of code.

Eiji

Eiji OP

Exactly, I was asking if this is too generic and therefore my use case should not be considered as a bad practice.

Including bad practices … For example I don’t have to follow credo -. I want to follow it. I was asking if what I’m thinking about makes sense.

Yeah, that’s why I’m not considering it in every case, but the one like this one i.e. when I want to evaluate code as-is without any bindings or expectations what is in the evaluated code especially if I have to collect data passed as macro arguments and create a nested structure. In example above if there would be no calls to a nested_dsl macro then nested_values stored in current Process would default to empty list [] as it was done before evaluating (just to simplify prepending to list i.e. [head | tail] notation). I’m not seeing a problem here, but still prefer to ask just in case.

Thanks for your opinion!

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews