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

Where Next?

Popular in Questions Top

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
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41539 114
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement