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.

Last Post!

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]

Where Next?

Popular in Questions Top

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
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
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
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
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
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

Other popular topics Top

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
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54996 245
New
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
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44608 311
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New

We're in Beta

About us Mission Statement