Compiler fails to find the struct module

Hey folks.

I’ve got a simple use case for macros based on the following example module:

defmodule StructTestWorking do
  defmodule State do
    defstruct []
  end

  def foo(%State{}), do: nil
end

I want to extract the function foo into a “template module” and then use that module:

defmodule StructTestCommonUsing do
  defmacro __using__(_opts) do
    quote do
      def foo(%State{}), do: nil
    end
  end
end

defmodule StructTestFailingUsing do
  defmodule State do
    defstruct []
  end

  use StructTestCommonUsing
end

As the module name suggests, this doesn’t work. I’m getting this compilation error:

== Compilation error on file lib/struct_test_failing_using.ex ==
** (CompileError) lib/struct_test_failing_using.ex:14: State.__struct__/0 is undefined, cannot expand struct State
    (stdlib) lists.erl:1353: :lists.mapfoldl/3

It doesn’t work when using a @before_compile hook either:

defmodule StructTestCommonBeforeCompile do
  defmacro __using__(_opts) do
    quote do
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def foo(%State{}), do: nil
    end
  end
end

defmodule StructTestFailingBeforeCompile do
  defmodule State do
    defstruct []
  end

  use StructTestCommonBeforeCompile
end

This is using Elixir v1.3.1. Would you expect this to work?

I’ve also pushed the code to a repo – https://github.com/alco/struct_test.

3 Likes

Instead of this, what happens if you try:

def foo(%__MODULE__.State{}), do: nil

EDIT: Actually that would not work either, what about this?

  defmacro __using__(_opts) do
    struct_name = Module.concat(__MODULE__, State)
    quote bind_quoted: [struct_name: struct_name] do
      def foo(%struct_name{}), do: nil
    end
  end

Or maybe:

  defmacro __using__(_opts) do
    struct_name = Module.concat(__MODULE__, State)
    quote bind_quoted: [struct_name: struct(struct_name)] do
      def foo(struct_name), do: nil
    end
  end

I’d have to experiment to really test, but off the top of my head maybe one of those helps you get further?

2 Likes

Maybe this issue might be related: https://github.com/elixir-lang/elixir/issues/4894

Have you tried with Elixir master?

2 Likes

I think that what is going on here, is that code like this:

defmodule Foo do
  defmodule Bar do
    defstruct [:baz]
  end
end

does the following:

  • It creates the module Foo
  • It creates the the module Foo.Bar
  • It creates the the struct Foo.Bar
  • It calls alias Foo.Bar at the end of the definition of the inner module, so you can ‘just’ use it as Bar in the outside module.

This alias is something that does not happen when you extract the functionality elsewhere.

What you might want to do instead, inside your __using__ definition, is to call the State struct as %__MODULE__.State{}, which will add the outer wrapping module to the struct name.

2 Likes

Using %__MODULE__.State{} does indeed work. What I find confusing is that the aliases in __ENV__ are the same both inside the quoted block and in the module definition itself:

defmodule StructTestCommonUsing do
  defmacro __using__(_opts) do
    quote do
      IO.puts "Env in the quote: #{inspect __ENV__.aliases}"
      def foo(%__MODULE__.State{}), do: nil
    end
  end
end

defmodule StructTestFailingUsing do
  defmodule State do
    defstruct []
  end

  IO.puts "Env in the module: #{inspect __ENV__.aliases}"

  use StructTestCommonUsing
end

This produces the following output during compilation:

Env in the module: [{State, StructTestFailingUsing.State}]
Env in the quote: [{State, StructTestFailingUsing.State}]

Apparently there’s some other compilation context that is not getting populated with the alias.

@michalmuskala I’ve tried Elixir master, it doesn’t make a difference.

1 Like

Aliases uses the environment outside of the quote. You don’t want alises in the environment where you inject code to affect the code in your quote. This is for the same reasons we have variable hygiene - you don’t want variables from another context to affect variables in the current context.

Check out the aliases hygiene section in the quote docs http://elixir-lang.org/docs/master/elixir/Kernel.SpecialForms.html#quote/2.

You cannot print __ENV__.alises at runtime like that to get the environment of the code at compile time. __MODULE__ works the same way when you think about it, it returns the module where the code is injected, so __ENV__ will also return the environment where the code is injected.

3 Likes

Ah fascinating, so my original thought was the correct way, I’ll need to remember that.

1 Like