`changeset()` spec requires nullable type which is not logically nullable

305 views Asked by At

I have the following code:

defmodule Foo do
  @moduledoc false

  use Ecto.Schema

  import Ecto.Changeset


  @type t :: %__MODULE__{
          id: integer(),
          foo: String.t(),
          baz_id: String.t(),
          bar: String.t() | nil
        }

  embedded_schema do
    field :foo, :string
    field :bar, :string
  end

  @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  def changeset(bae \\ %__MODULE__{}, attrs) do
    bae
    |> cast(attrs, @fields)
    |> unique_constraint(:baz_id)
  end
end

foo and baz_id should not be nil, as per the @type definition. However, dialyzer is complaining (with the given @spec) because the default value %__MODULE__{} will set them to nil.

If I replace the @type definition with:

...
  @type t :: %__MODULE__{
          id: integer() | nil,
          foo: String.t() | nil,
          baz_id: String.t() | nil,
          bar: String.t() | nil
        }
...

then dialyzer will not complain, but I am no longer capturing the idea that some of the fields are not nullable.

What would be an elegant way to make changeset() work the way it currently is, and avoid having dialyzer complaining for this specific use?

1

There are 1 answers

0
Aleksei Matiushkin On BEST ANSWER

Well, you’re explicitly specifying the default argument that violates a dialyzer contract (the schema is a bare struct underneath without default values,) that’s why dialyzer complains.

It is unclear, how you do suppose to handle an empty %__MODULE__{} once it’s not allowed, but answering the question stated, the workaround would be to accept nil argument as a default.

@spec changeset(
    nil | t() | Ecto.Changeset.t(), map()
  ) :: Ecto.Changeset.t()
def changeset(bae \\ nil, attrs) do
  bae
  |> Kernel.||(%__MODULE__{})
  |> cast(attrs, @fields)
  |> unique_constraint(:baz_id)
end