sodapopcan

sodapopcan

Defining functions in a loop inside a macro

I can dynamically define functions in a “loop” like this:

defmodule Foo do
  for {f, i} <- [{:foo, "foo"}, {:bar, "bar"}] do
    def foo(unquote(f)) do
      unquote(i)
    end
  end
end

iex> Foo.foo(:foo)
"foo"
iex> Foo.foo(:bar)
"bar"
iex> Foo.foo(:baz)
💣

So that’s all cool :+1:

Now how do I do this inside of a macro? I’ve been at this for a few hours now and having a very hard time wrapping my head around it. The closest I’ve come is this:

defmodule Foo do
  defmacro __using__(_) do
    quote do
      for {f, i} <- [{:foo, "foo"}, {:bar, "bar"}] do
        defmacro foo(f) do
          quote do
            # unquote(i)  <- This doesn't work so I'm commenting it out for now
          end
        end
      end
    end
  end
end

This has the effect of defining def foo(:foo) twice, even though I dbg(f) in inside the inner macro and it has the correct values. On top of that, I can’t access i inside the inner macro.

I got a bit of hint from this thread. I tried re-quoting that whole comprehension, capturing it in a variable, then quote(do: unquote_splicing(block)) it, but that didn’t work. There is more code that follows it so, I dunno.

I appreciate any help though if this serves as a rubber duck that’d be good too :slight_smile:

Marked As Solved

kip

kip

ex_cldr Core Team

When you have nested quote blocks, you need to give Elixir a hint as to what an unuquote applies to. You do this by passing quote unquote: false to the outer block. The following compiles, but I haven’t tested if its actually what you want:

iex(1)> defmodule Foo do
...(1)>   defmacro __using__(_) do
...(1)>     quote unquote: false do
...(1)>       for {f, i} <- [{:foo, "foo"}, {:bar, "bar"}] do
...(1)>         defmacro foo(f) do
...(1)>           quote do
...(1)>             unquote(i)
...(1)>           end
...(1)>         end
...(1)>       end
...(1)>     end
...(1)>   end
...(1)> end
{:module, Foo,
 <<70, 79, 82, 49, 0, 0, 6, 168, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 177,
   0, 0, 0, 17, 10, 69, 108, 105, 120, 105, 114, 46, 70, 111, 111, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, {:__using__, 1}}

Also Liked

christhekeele

christhekeele

It’s worth remembering that the return value of a macro just needs to be a valid AST data structure to be inserted somewhere else, which can be composed however you like, including raw literals! It’s easy to forget sometimes, because quote is so powerful, and I often fall into this block-based thinking where the final expression of a defmacro has to be a top-level quote block.

However, an equally valid return value is a list of AST snippets, which will get interpreted as a series of sequential expressions. So you can just tuck the quote inside the for, and return a list of quoted expressions:

defmodule Foo do
  defmacro __using__(_opts \\ []) do
    for {function_argument, function_return} <- [{:fizz, "fizz"}, {:buzz, "buzz"}] do
      quote do
        def baz(unquote(function_argument)) do
          unquote(function_return)
        end
      end
    end
  end
end

defmodule Bar do
  use Foo
end

Bar.baz(:fizz)
#=> "fizz"
sodapopcan

sodapopcan

I am quickly realizing this :sweat_smile: I think I’m going to explore other options.

EDIT: Ya, re-reading the docs they are consolidated after everything else is compiled, so what I want to do is not possible. I have a runtime version of it working so I will probably just stick with that for now.

Thanks again for your help!

sodapopcan

sodapopcan

Whoa! With a slight tweak that worked! Thanks so much, Kip! Though now I’m puzzled as to how it works with protocol impls, though the docs do qualify that they are usually consolidated after the app is compiled. I’m guessing if there is a compile-time dependency on it that will make it happen earlier. That is maybe supported by this but I haven’t taken the time to grok it yet.

Thanks again!

defmacro __using__(_) do
  func =
    quote unquote: false do
      {_, mods} = Proto.__protocol__(:impls)

      for mod <- mods do
        type = mod.__struct__
        name = Proto.name(type)
        ast = Proto.body(type)

        def func(unquote(name)) do
          unquote(ast)
        end
      end
    end

  quote do
    unquote(func)

    # more stuff that depends on func/1
  end
end
sodapopcan

sodapopcan

Hey Chris! So had I tried something like this but of course I didn’t give the whole picture in my post. I’m iterating over protocol impls, so they need to be retrieved in a quote block in order for it to be consolidated (as far as I can tell, at least). However, you’ve definitely given me a good hint here. I assume I’m going to have to iterate over ast and inject it. Something like:

defmacro __using__(_) do
  {_, _, result_ast} =
    quote do
      {:consolidated, mods} = P.__protocol__(:impls)

      for mod <- mods do
        type = mod.__struct__
        {P.name(type), P.body(type)}
      end
    end

  result =
    for ast <- result_ast do
      quote do
        # Profit
      end
    end

  quote do
    unquote(result)
  end
end

Something like that? In any event, you’ve given me a good hint for a way forward. Thanks!!

I’ll report back if I get this working.

christhekeele

christhekeele

I suspect it’s just a race condition in the compiler, and beating it is a matter of project size.

Not to X Y problem you, but why are you trying to metaprogram generated functions based on structs implementing protocols? I ask for three reasons,

  1. Not having to know in advance at compile time what modules offer an implementation is part of the point of protocols, so I am curious as to your usecase
  2. I am even more curious to your usecase, as I like committing macro crimes
  3. I know of some macro crimes that may help, depending on the Y of your problem

Where Next?

Popular in Questions Top

JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement