How to return literals with macros?

Hello,

I’m having some hard time trying to figure out how macros are working…

Let’s say I have some code where a certain variable is already defined (eg. x) and where I’m using this variable within an expression that I have several times:

...
x = "Result:"
...
x <> a_function_outputting("a string")
...
x <> another_function_outputting("a string")
...

For convenience I wanted to define a function that will encapsulate this calling.
This concatenation is an example, of course.
Now since I never know what are the functions I can call alongside the repeating x <>
I wanted to define a macro that will expand appropriately to the above starting from some code like this:

print_x(a_function_outputting("a string"))
print_x(another_function_outputting("a string"))

When I do the following:

defmacro print_x(function) do
  x <> function
end

I got the error that x is not defined here.

I tried many combination with quoteing, unquoteing and even Macro.escapeing.
But I wasn’t able to achieve what I want.

In fact what I want is to get the literals like how #define works in C.

NB: In order to avoid XY problem.
I want to achieve this kind of macro in Phoenix views where the function are helpers functions and where the variable is an assign (so it will even be @x rather than x)

Thank you very much for any details.

As an assign is not statically known at compile time, you can not create a macro that would return the value of that assign as a literal.

2 Likes

Is it applicable for regular variable too?

I mean is it possible to do something equivalent to bare text replacement…
It’s like code source string processing, no value has to be replaced at compile time…
I know that this example is somehow confuse and I also did a poor job explaining the problem, but I thought that this could be possible and that I’m missing a part…

You can not create literals from unknown values.

If everything you receive as argument is known at compile time, you can use Code.eval_quoted or similar to evaluate the AST passed in. Though if that does not evaluate to a final value, you are out of luck.

If then you have 2 things that evaluate to a binary, you can return that binaries AST, which in most situations is equivalent with writing that binary out in code.

1 Like

Yes I noticed that.

However, just to remove any confusion before I leave since what’s your saying is clear.

I don’t want to replace with the content of said variables (referenced by x for example), but literally end up with the variable name in the code as a literal.
It’s like being able to quote what ever you want since at that moment nothing is executed, just the AST being generated…

For example, what I want to do (replacing some code by other bit of codes without anything coming from runtime) is something I can do with pure string manipulation, eg. in bash or ruby or even sed. So there’s nothing related to runtime code.

This is why I considered this could be feasible with macro, and that I was simply lost because of my lack of knowledge of how the syntax of macros works.

Btw, thank you for your time…

If I’ve understood correctly you are saying you want to transform:

x = a_variable

into

x = "a_variable"

Is that correct?

1 Like

Hi @kip!

Not really…
I knew that I might be confusing…

I want to transform:

my_func(arbitrary_func(arbitrary_parameters))

into

@x && arbitrary_func(arbitrary_parameters))

So in other words I want to end up with this code where that @x is always @x (it’s assign in Phoenix Views) so it’s not the responsibility of that macro to know if that assign is available or not.

But it might be anything else, like a function composition like:

arbitrary_func(arbitrary_parameters)
|> IO.inspect

I simply want to be able to build helper function that keep everything from what’s being called at the first place (arbitrary_func(arbitrary_params))

In C #define and other preprocessor macro are just doing that, code string replacement.
Nothing to do with runtime code, variable value etc.

But I don’t have any success achieving that.

I think that the thing you’re looking for is var.

If I’m right about what you mean, you’re using the word “literal” in a rather non-standard way, and what you actually want is a macro that’s non-hygienic.

The way I think you want to write this macro is like this:

import Macro, only: [var: 2]

defmacro append_to_x(y) do
  quote do
    unquote(var(:x, nil)) <> unquote(y)
  end
end

With this append_to_x macro, writing e.g. append_to_x(one(two(three()))) is equivalent to x <> one(two(three())) — it uses whatever x is in scope at the site where it is used, much like a #define in C.

All this said—var is, as the name implies, for variables, and this won’t work if what you actually want is to reference the assign @x.

Luckily, assigns is itself a variable in scope in eelixir templates, so you can access @x also as assigns.x. That means you probably will want to do something like this:

import Macro, only: [var: 2]

defmacro append_to_x(y) do
  quote do
    unquote(var(:assigns, nil)).x <> unquote(y)
  end
end

I hope this helps—it’s my best guess as to what you actually want, although I may well have misunderstood the intent of your question (it’s not completely clear.)

2 Likes

Hello,

Thanks for the details!

For me a literal is a “constant” but in its context, for example:

a = "x"
x = "x"

Here in the first line a is obviously a variable, and x is a literal.
In the seconde line the lhs x is also a variable when the rhs x is a literal.

However in my case, when I want to end up from

my_func(arbitrary_func(arbitrary_parameters))

to

@x && arbitrary_func(arbitrary_parameters))

I want to end up with @x used as a literal in the code in which it will end up to what it means in the runtime, i.e. the assign @x.

To be more clear, let’s assume I have this code in all my view files:

custom_log(IO.inspect value)
..
custom_log(error_message)
...
custom_log(IO.inspect(reason, label: "Reason"))
custom_log(IO.inspect reason, label: "Reason")

(I deliberately put different things)

Now I can “preprocess” that file with a script in any language.
Here an example in JS with a simple regex replace:

//Simple example working only if there is at most one nested parentheses, but that''s okay in my case
my_file_content.replace(/custom_log\((.*\)?\))/g, "@x && $1")

In C it’s easy to do so with macros and #define.
I think that it’s something easily doable with elixir Macro…

Really it’s just static string replacement…
I don’t know how to explain this in other words…

Anyway, I tried with your example and some other variants using var.
And I got the same error as assigns not being available (expanded as assigns() and function undefined)

As I said, my current use case is to use it in that form with helper functions in LiveView views.
The purpose of @x && whatever is that here x is something like a fingerprint and when it’s changing I force the rerendering of whatever.

See this message on another topic for more context…

I hope my intent would be clear right now…

You may be able to inspect the AST and infer the value, which can be trivial if the rhs is a literal. For instance:

defmacro myblock(do: block) do
  IO.inspect(block)
end
myblock do     
  a = 123
end

Your macro will then get {:=, [line: ...], [{:a, [line: ...], nil}, 123]}

Are you trying to emulate dynamic scoping with macros? You can (but probably shouldn’t) do ugly stuff like this:

defmacro foo(symbol) do
  quote do
    unquote({symbol, [], nil}) + 321
  end
end

def bar() do
  x = 123
  foo(:x)
end

bar() will evaluate to 444 in this case.

Hi…
Ok so @zzq might be right in my wrong usage of the word “literal” since you also thought about “value” literals…

But this is not the case… It’s not even about affectation (or binding).

And just a quick note to @kip and @NobbZ I put an example of what I want to do in that discussion, ie. extracting a call to gettext (among other function calls) combined with the && operator within a simplified call like this:

# Here I'm using the letter l as the name of the macro as a shorthand for live..
l(gettext "Hello World")
# I wanted to be converted into:
@x && (gettext "Hello World")

The reason I’m talking about literal so far is that I want to handle the argument of the function (in this case gettext "Hello World" as well as the @x as is and reproduce them in the same context.

Also I could simply do that:

def l(x, text) do
  x && gettext(text)
end

and use it like so:

l(x, "Hello World")

However gettext doesn’t allow dynamic translation.
So I must leave it with the literals (and I think that here it’s the correct usage of the word :slight_smile:

I hope that does make sense…

I am not sure I understand what you are trying to do to be honest, but here goes another guess:

defmacro l(x, expr) do
  x && expr
end

and then l(x, gettext "Hello World")

2 Likes

Hi again…

Since I was trying to embed that @x too, I mean calling like l(gettext "Hello World"), I didn’t even think to try with leaving it like you did.
And actually this works!

And I guess that now you understand what I wanted to do…

Since I’m getting closer I tried to generate that call to the variable like so:

defmacro l(expr) do
  {:@, [context: Elixir, import: Kernel], [{:x, [context: Elixir], Elixir}]} && expr
end

But it’s still not workin.
That bit of code (lhs of &&) is the result of quote do: @x

Something like this?

defmodule MyMacro do
  defmacro l(expr) do
    quote do
      @x <> unquote(expr)
    end
  end
end

defmodule MyMod do
  require MyMacro
  @x "x"

  def my_fun(arg) do
    MyMacro.l(arg)
  end
end

In iex:

iex> MyMod.my_fun("y")
"xy"

Hi @kip,
Thanks for the input…
This is not working because in my case the call to that function (the one you did in iex MyMod.my_fun("y") is done in a LiveView.
But the macro is expanded in the context of the render function still in the LiveView module (not inside the view) so it’s considered as a module attribute.
And thus I get the error that the module attribute is not defined.

I already tried like @zzq suggested in his first message to use assigns.x, but the problem is still the same since even socket nor assigns are available in the view… Otherwise, indeed this is working.

Now this makes me wondering, how the conversion of a regular variable (e.g. in mount() or render()) into an assign with the @xyz is done…
And if there isn’t a way to access regular variable in the views…

I still don’t understand what you’re trying to do, can you show us an actual example?

EEx includes special code to handle @x-style access to assigns[:x] - source:

You might have better results producing that kind of output, using var!(assigns)

I don’t really understand what you want, but it looks like that you are looking for unhygienic macro.

defmodule UnhygienicMacro do

  defmacro print_x(whatever) do
    quote do
      IO.puts "#{inspect(var!(x))}: #{inspect(unquote(whatever))}"
    end
  end

end

and then

iex(1)> import UnhygienicMacro
UnhygienicMacro
iex(2)> print_x "hello"
** (CompileError) iex:2: expected "x" to expand to an existing variable or be part of a match
    (elixir 1.10.4) expanding macro: Kernel.var!/1
    iex:2: (file)
    (elixir 1.10.4) expanding macro: Kernel.to_string/1
    iex:2: (file)
    expanding macro: UnhygienicMacro.print_x/1
    iex:2: (file)
iex(2)> x = 42
42
iex(3)> print_x "hello"
42: "hello"
:ok