mruoss

mruoss

__using__ macro with option

Hi community!

What is the correct way to implement the __using__ macro with an option that changes the way generated functions behave? Or is that a no-go by itself?

An example:

defmodule UsedModule do
  defmacro __using__(opts) do
    quote do
      if Keyword.get(unquote(opts), :with_extra, false) do
        def extra(), do: "with extra"
      else
        def extra(), do: "no extra"
      end
    end
  end
end
defmodule UsingModuleWithExtra, do: use UsedModule, with_extra: true
defmodule UsingModuleNoExtra, do: use UsedModule
iex> UsingModuleWithExtra.extra()
"with extra"

iex> UsingModuleNoExtra.extra()
"no extra"

My question: Is this the way to go inside the __using__ macro? Or would you define the extra function once and do the if inside like this?

def extra() do
  if Keyword.get(unquote(opts), :with_extra, false), 
    do: "with extra", 
    else: "no extra"
end

Or is there a cleaner way I’m not seeing?

Thanks!

Marked As Solved

Eiji

Eiji

In case you have a static output like in this example then you can simply call unquote(string) and outside quote block set string variable based on option.

defmodule UsedModule do
  defmacro __using__(opts) do
    string = if opts[:with_extra], do: "with extra", else: "no extra"
    quote do
      def extra(), do: unquote(string)
    end
  end
end

In other case I would recommend to simply move if condition outside quote block and simply use 2 quote calls.

defmodule UsedModule do
  defmacro __using__(opts) do
    if opts[:with_extra] do
      quote do
        def extra(), do: "with extra"
      end
    else
      quote do
        def extra(), do: "no extra"
      end
    end
  end
end

Note: If opts[:with_extra] does not exists then you got nil which is falsy value and therefore we do not need a Keyword.get/3 call here. :smiling_imp:

Also Liked

Eiji

Eiji

Yeah, I can say that it may depend on case. For example there is a big difference working on raw data passed to macro and variables passed to macro even if their declaration was static one line above …

some_macro("some value")
# param value in macro: "some value"

value = "some value"
some_macro(value)
# param value in macro: {:value, […], nil}

unquote inside quote block solves the problem. However in some cases you may expect some value to be passed raw instead.

some_value = true

quote do
  if unquote(some_value) do
    :ok
  else
    :error
  end
end

Here since the value is known “outside” the half of code we generate would never be used.

A simple example shows how important it may be:

defmodule MyMacro do
  defmacro __using__(opts) do
    Enum.map(opts[:enabled_features] || [], &apply_feature/1)
  end

  defp apply_feature(:feature_name) do
    quote do
      # …
    end
  end

  # …
end

Look that if we would have only 1/10 features enabled then we would generate only one quote block.

However if we would write it like this:

defmodule MyMacro do
  @all_features [:feature_name, # …]

  defmacro __using__(opts) do
    quote do
      import MyMacro, only: [apply_feature: 1]

      for feature <- unquote(@all_features), feature in unquote(opts[:features] || []) do
        apply_feature(feature)
      end
    end
  end

  defmacro apply_feature(:feature_name) do
    quote do
      # …
    end
  end

  # …
end

then said module with one enabled feature would have generated extra loop with 9/10 skipped elements. Also in example above apply_feature/1 is a public macro for absolutely no reason (comparing to previous example).

Also it would be nice to provide a link for mentioned text as José could talk about other case or just more complex example. It would be good for others to see all sides of this coin. :slight_smile:

Eiji

Eiji

If it’s stupid, but it works then it’s no stupid! :smiling_imp:

I have used atom as it’s best for what I called feature_name as those in apply_feature/1 are known at compile-time. You can use strings, integers and every other data that you can write pattern-matching for.

Also take a look at example below:

some_macro(["a", "b", "c"])
# param value in macro
# ["a", "b", "c"]

some_macro(~w[a b c])
# param value in macro
# {:sigil_w, [delimiter: "[", context: Elixir, imports: [{2, Kernel}]],
#  [{:<<>>, [], ["a b c"]}, []]}

some_macro(~w[a b c])
# value of `Macro.expand(param, __CALLER__)`
# ["a b c"]

If you want to call String.to_atom/1 inside macro then:

  1. You can call Enum.map(list, &String.to_atom/1) as long as you require list of atoms passed as raw list or sigil (see above example)
  2. In function definition String.to_atom/1 call needs to be passed inside unquote/1 call.
  3. Inside module and function block you can use String.to_atom/1 call as well inside unquote/1 call as outside unquote/1 call.

Hope that helps.

Where Next?

Popular in Questions Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is quite...
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
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; somethi...
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
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
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

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
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
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
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
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
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement