mruoss

mruoss

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!

Showing Posts 1 to 7

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:

mruoss

mruoss OP

Thanks for your answer. I mean to remember heaving read @josevalim mentioning somewhere that there should be no code (or as little as possible?) outside the quote block. But it might be applicable only in that context which obviously I don’t remember. :roll_eyes:

Anyway this helps, thanks. Also for the note! :smiley:

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:

mruoss

mruoss OP

If I ever find that text again, I will definitely add a link to it here.

But your example with the 10 features only works because a quoted list of atoms is still a list of atoms, right? If for some stupid reason I decide to pass enabled_features: [String.to_atom("my_frature")], it would not work?

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.

mruoss

mruoss OP

Not maps. Or structs. You can’t expand those to their original value either. You’d have to call Code.eval_quoted() which is discouraged inside macros, no?

I’d like come back to my original question. I have changed the example to take a map. It does not make much sense like this and could be done better, but it is a simple example to show the case with a map as option value:

defmodule UsedModule do
  defmacro __using__(opts) do
    quote do
      if unquote(opts[:with_extra]) do
        def extra(), do: "with extra"
      else
        def extra(), do: "no extra"
      end
    end
  end
end
defmodule UsingModuleWithExtra, do: use UsedModule, config: %{with_extra: true}
defmodule UsingModuleNoExtra, do: use UsedModule
iex> UsingModuleWithExtra.extra()
"with extra"

iex> UsingModuleNoExtra.extra()
"no extra"

See the passed option is expected to be a map. Which cannot be expanded to its original value. You have to unquote them inside a quote block (right?). So my question was, if you would - in this case where the argument requires unquoting - implement the macro like I did above or rather put the if condition inside the generated function like this:

def extra() do
  if Keyword.get(opts, :config, %{})[:with_extra], 
    do: "with extra", 
    else: "no extra"
end
Eiji

Eiji

You can’t expand them, because unlike sigil they are special forms. However if you still expect to pass raw map then you can solve that in few ways depends on use case …

defmodule UsedModule do
  defmacro __using__(opts) do
    if get_config!(opts, __CALLER__).with_extra do
      quote do
        def extra(), do: "with extra"
      end
    else
      quote do
        def extra(), do: "no extra"
      end
    end
  end

  defp get_config!(opts, env) do
    case opts[:config] do
      nil ->
        raise ":config option not passed"

      {:%, _, [{:__aliases__, _, [:PatternMatchingConfig]}, {:%{}, _, config}]} ->
        struct(PatternMatchingConfig, config)

      {:%, _, [aliases, {:%{}, _, config}]} ->
        aliases |> Macro.expand_literal(env) |> struct(config)

      {:%{}, _, config} ->
        Map.new(config)

      _config ->
        raise "unexpected config value. Did you passed a raw map?"
    end
  end
end

defmodule ExpandedConfig do
  defstruct with_extra: false
end

defmodule PatternMatchingConfig do
  defstruct with_extra: false
end

defmodule Examples do
  defmodule ExpandedStruct do
    use UsedModule, config: %ExpandedConfig{}
  end

  defmodule PatternMatchingMap do
    use UsedModule, config: %{with_extra: true}
  end

  defmodule PatternMatchingStruct do
    use UsedModule, config: %PatternMatchingConfig{}
  end
end

If you do not want to mix your code with handling special forms then all you need to do is write something like:

defmodule Example do
  def sample(ast, env) do
    if Macro.quoted_literal?(ast) do
      Macro.postwalk(ast, fn
        {:__aliases__, _, _} = alias -> Macro.expand(alias, env)
        {:%{}, [], opts} -> Map.new(opts)
        {:%, [], [module, opts]} -> struct(module, opts)
        other -> other
      end)
    else
      ast
    end
  end
end

iex> map_ast = quote do: %{day: 1, month: 1, year: 2022}
{:%{}, [], [day: 1, month: 1, year: 2022]}

iex> Example.sample(map_ast, __ENV__)
%{day: 1, month: 1, year: 2022}

iex> struct_ast = quote do: %Date{day: 1, month: 1, year: 2022}
{:%, [],
 [
   {:__aliases__, [alias: false], [:Date]},
   {:%{}, [], [day: 1, month: 1, year: 2022]}
 ]}

iex> Example.sample(struct_ast, __ENV__)
~D[2022-01-01]
— All posts loaded —

Where Next? Top

Trending in Questions Top

water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New

Other Trending Topics Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews