Elixir + Phoenix : __MODULE__ undefined inside a quote

861 Views Asked by At

(code citation has been anonymized)

In my phoenix models I have some methods which are redundant, like this basic one :

  def build(params) do
    changeset(%__MODULE__{}, params)
  end

Since I put them inside my model modules, they work fine, but I want to avoid code duplication and I want to make them available to all my models through a helper module like this :

defmodule MyApp.Helpers.Model do
  defmodule Changeset do
    defmacro __using__(_opts) do
      quote do
        def build(params) do
          changeset(%__MODULE__{}, params)
        end
      end
    end
  end
end

Doing this, I'm getting an error :

== Compilation error on file lib/my_app/model/my_model.ex ==
** (CompileError) lib/my_app/model/my_model.ex:3: MyApp.Model.MyModel.__struct__/1 is undefined, cannot expand struct MyApp.Model.MyModel
    (stdlib) lists.erl:1354: :lists.mapfoldl/3

The related model looks basically like this :

defmodule MyApp.Model.MyModel do
  use MyApp.Helpers, :model
  use MyApp.Helpers.Model.Changeset # here for comprehension, should be in MyApp.Helpers quoted :model method

  schema "my_table" do
    field :name, :string

    timestamps()
  end

  @required_fields ~w(name)a
  @optional_fields ~w()
  @derive {Poison.Encoder, only: [:name]}

  def changeset(model, params \\ %{}) do
    model
    |> cast(params, @required_fields)
    |> cast(params, @optional_fields)
    |> validate_required(@required_fields)
    |> validate_format(:name, ~r/^[a-z]{3,}$/)
    |> unique_constraint(:name)
  end
end

I'm thinking that's because the module is not yet defined at compile time within the macro, but I'm not sure, nor how to fix this and make it works.

Some lights here would be much appreciated, thanks.

1

There are 1 best solutions below

1
On BEST ANSWER

The problem is that the struct is defined by calling the defstruct macro, and cannot be used earlier, since the compiler has no idea how to expand it. In case of ecto schemas the struct is declared by the schema macro underneath.

Fortunately, looking the documentation of defstruct we can see it creates a function called __struct__/0 on the module where the struct is declared. And functions can call other local functions, even before they are defined! Using that knowledge we can change your macro to:

defmodule MyApp.Helpers.Model do
  defmodule Changeset do
    defmacro __using__(_opts) do
      quote do
        def build(params) do
          changeset(__struct__(), params)
        end
      end
    end
  end
end

That said, defining functions in __using__ is generally considered a bad practice, as stated in the documentation for Kernel.use/1

Finally, developers should also avoid defining functions inside the __using__/1 callback, unless those functions are the default implementation of a previously defined @callback or are functions meant to be overridden (see defoverridable/1. Even in these cases, defining functions should be seen as a “last resource”.

Defining functions in __using__ has many downsides including: slow compilation (the function is compiled over and over in every module it's injected to), difficulty in debugging ("where is this coming from?") and poor compostability.

A much better approach might be to define a single, reusable function. For example:

defmodule MyApp.SchemaUtils do
  def build(schema, params) do
    schema.changeset(struct(schema), params)
  end
end

PS. The @derive calls have to be declared before the struct.