Unable to spec function return type of `typedstruct` struct associated with module

I’m unable to have mix dialyzer come up clean. Specifically this function spec has me stumped:

If I change the return type to any() it comes up clean. The dialyzer error is:

lib/kojin/rust/field.ex:67:invalid_contract
The @spec for the function does not match the success typing of the function.

Function:
Kojin.Rust.Field.field/4

Success typing:
@spec field(atom() | binary(), atom() | binary() | %Kojin.Rust.Type{_ => _}, binary(), Keyword.t()) ::
  %Kojin.Rust.Field{
    :access => _,
    :doc => binary(),
    :name => binary(),
    :type => nil | %Kojin.Rust.Type{_ => _},
    :visibility => _
  }

Any suggestions appreciated!

Thanks,
Dan

Compare dialyzer’s success typing:

  %Kojin.Rust.Field{
    :access => _,
    :doc => binary(),
    :name => binary(),
    :type => nil | %Kojin.Rust.Type{_ => _},
    :visibility => _
  }

with your struct definition:

  typedstruct enforce: true do
    field(:name, atom)
    field(:doc, String.t(), enforce: false)
    field(:type, Type.t())
    field(:access, atom | nil)
    field(:visibility, atom, default: :pub)
  end

You can see that fields :name and :type have different types.

1 Like

I created a PR to fix it. I think you may want to create a @type field_name :: atom | binary and use that for consistency. Here’s a PR that fixes the dialyzer issue:

Also I’d generally recommend using String.t() instead of binary if you’re expecting to be dealing with UTF8 strings since String.t() is more descriptive to the reader (even though it’s just an alias for binary). Also I’d settle on either keyword or Keyword.t(), you seem to be using both of them.

3 Likes

Thank you. I need more practice interpreting dialyzer.

1 Like

You are not alone in this. Reading dialyzer output is like listening to a drunk oracle.

2 Likes

@axelson - I’m sorry, I am not yet seeing how this can be true:

`Kojin.require_snake/1` said that it returned only `binary` but
sometimes it returns an atom.

How can this return an atom:

@spec require_snake(atom | binary) :: atom | binary
  @doc ~s"""
  Ensures the name is snake case, raises `ArgumentError` if not.

  ## Examples

      iex> assert_raise(ArgumentError, "Name must be snake: `FooBar`", fn -> Kojin.require_snake(:FooBar) end)
      %ArgumentError{message: "Name must be snake: `FooBar`"}

      iex> Kojin.require_snake(:foo_bar)
      "foo_bar"
  """
  def require_snake(name) when is_binary(name) do
    if !Kojin.Id.is_snake(name) do
      raise ArgumentError, "Name must be snake: `#{name}`"
    end

    name
  end

  def require_snake(name) when is_atom(name), do: require_snake(Atom.to_string(name))

The first always returns name, which is binary (unless it raises an ArgumentError). The second only accepts an atom and calls the first which only returns a binary.

I appreciate your help here. I want to love elixir like others do and right now for me @spec seems to be something I need to learn much more about to get that level of comfort.

BTW: I’ll take your suggestions on keyword vs Keyword.t(). My inconsistency stems from not really knowing what I’m doing, but trying things anyway.

Ah, looks like you’re correct, require_snake/1 always returns binary. I think I misread the second clause.

1 Like

Throwing an error has a special type - no_return(), As far as I know, if function possibly could raise - then possible there will be no_return() output.
However, in those cases dialyzer complains with a message saying that “function has no local return”. I wonder if adding no_return() to possible output types helps.

actually never mind… Just checked. Seems like no_return() is not necessary for dialyzer.