(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.
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 theschema
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:That said, defining functions in
__using__
is generally considered a bad practice, as stated in the documentation forKernel.use/1
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:
PS. The
@derive
calls have to be declared before the struct.