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

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
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
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
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 Postg...
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
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New

Other popular topics Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement