prook

prook

Confused about spec/dialyxir: "Invalid type specification for function"

Hi,

I’m trying to get hang of specs, but I’ve hit a wall. I’m writing a thin facade over Nerves’ SystemRegistry, basically stuff like:

defmodule ConfigRegistry do
  @spec update(SystemRegistry.scope(), any) :: {:ok, {map, map}} | {:error, term}
  def update(scope, value) do
    SystemRegistry.update([:config | scope], value)
  end
end

Running mix dialyzer on that results in

lib/config_registry.ex:2:invalid_contract
Invalid type specification for function.

Function:
ConfigRegistry.update/2

Success typing:
@spec update([any()], _) :: %SystemRegistry.Transaction{
  :delete_nodes => MapSet.t(_),
  :deletes => MapSet.t(_),
  :key => _,
  :opts => Keyword.t(),
  :pid => pid(),
  :update_nodes => MapSet.t(_),
  :updates => map()
}

A quick look at

https://github.com/nerves-project/system_registry/blob/master/lib/system_registry.ex#L54

tells me there are two updates, one that can be used standalone (this is the one I’m interested in), and the other one for use with an open transaction.

The functions are specced like this:

@spec update(one, scope, value :: any) :: Transaction.t() when one: Transaction.t()
@spec update(one, value :: any, opts :: Keyword.t()) ::
          {:ok, {new :: map, old :: map}} | {:error, term}
        when one: scope

I can sort of understand dialyxir’s complaint, if I make an assumtion that it cannot recognize [:config | scope] as something that is not a SystemRegistry.Transaction.t(). But that assumption seems very wrong.

So… Either (and most likely) I’m missing some crucial and basic knowledge of spec, there is an error in SystemRegistry.update spec, or a bug in Dialixyr.

How can I persuade Dialyxir my code will always call the standalone update function?

Marked As Solved

prook

prook

(8 hours ago: Hmmm, I should probably do that…)

This has gotten interesting. First, I’ve reduced the warning-inducing code to this:

defmodule WhatTheSpec do
  @type key :: atom()
  @type transaction :: map()

  def put(_, _, _ \\ nil)

  @spec put(one, key(), any()) :: transaction() when one: transaction()
  def put(t, _, _) when is_map(t), do: t

  @spec put(one, any(), Keyword.t()) :: :ok when one: key()
  def put(_, _, _), do: :ok

  @spec put_foo(any()) :: :ok
  def put_foo(value) do
    put(:foo, value)
  end
end

Next, I realized that Keyword.t() does not include nil, yet the third parameter may be nil indeed. So the spec

@spec put(one, any(), Keyword.t()) :: :ok when one: key()

should actually be

@spec put(one, any(), Keyword.t() | nil) :: :ok when one: key()

And sure enough, Dialyxir goes green! So it was lack of understanding on my part after all! Also, this means there’s a similar problem in SystemRegistry @spec of update/3, which sent me on this trip! And furthermore, I point my finger at Dialyxir as well! *gasp!*

Dialyxir’s warning is misleading to say the least:

lib/spec.ex:13:invalid_contract
Invalid type specification for function.

Function:
WhatTheSpec.put_foo/1

Success typing:
@spec put_foo(_) :: map()

As I grok it, @spec put_foo(_) :: map() is not a success typing, as there is actually none. I reason this is a bug, as the complaint about map() being a return type goes away after the | nil fix. It should either stay there (no, it should not!), or never appear at all, right?

Also, changing update_foo from the original example (i. e., before the | nil fix) to

  @spec put_foo(any()) :: :ok
  def put_foo(value) do
    put(:foo, value, nil) # note the explicit nil here
  end

makes Dialyxir barf completely:

Please file a bug in Issues · jeremyjh/dialyxir · GitHub with this message.

Failed to format warning:
@spec a(:one, any(), Keyword.t()) :: 'ok’whenone :: key()\ndef a() do\n :ok\nend\n”

Legacy warning:
lib/spec.ex:7: The contract ‘Elixir.WhatTheSpec’:put(one,key(),any()) → transaction() when one :: transaction();(one,any(),‘Elixir.Keyword’:t()) → ‘ok’ when one :: key() cannot be right because the inferred return for put(‘foo’,_value@1::any(),‘nil’) on line 15 is ‘ok’ | map()


lib/spec.ex:14:no_return
Function put_foo/1 has no local return.


@jeremyjh, It seems I’ve hit some murky parts of Dialyxir with this. :slight_smile:

Also Liked

peerreynders

peerreynders

This is where your mental model breaks down: there is only and there can only be one update/3 function. That function may be implemented in terms of multiple function clauses - but they all belong to the same function.

The Erlang style of specs make that much clearer:

-spec foo(pos_integer()) -> pos_integer()
         ; (integer()) -> integer().

So strictly speaking the return type of SystemRegistry.update/3 is the sum type Transaction.t() | {:ok, {new :: map, old :: map}} | {:error, term}.

A more type-aware module API would have

  • SystemRegistry.update_with_scope/3
  • SystemRegistry.update_with_transaction/3
  • And a convenience function SystemRegistry.update/3 that delegates appropriately.

but given that Erlang/Elixir is dynamically typed you will run into functions that have only been typed after the fact rather than being designed with types in mind.

peerreynders

peerreynders

Tried this approach?

  def update(scope, value) do
    case SystemRegistry.update([:config | scope], value) do
      %SystemRegistry.Transaction{} ->
        {:error, :undefined} # i.e. this will never happen
      result ->
        result
    end
  end

or even more explicitly, intention revealing

  @spec update(SystemRegistry.scope(), any) :: {:ok, {map, map}} | {:error, term}
  def update(scope, value),
    do: update_with_scope(scope, value)

  @spec update_with_scope(SystemRegistry.scope(), any) :: {:ok, {map, map}} | {:error, term}
  defp update_with_scope(scope, value) do
    case SystemRegistry.update([:config | scope], value) do
      %SystemRegistry.Transaction{} ->
        {:error, :undefined} # i.e. this will never happen
      result ->
        result
    end
  end
jeremyjh

jeremyjh

Everything is worth fixing, and I’ve found the Erlang group to be very responsive and helpful on past Dialyzer bugs. My only hesitation again is that its possible the when spec doesn’t really intend to behave like we think it would. I’d probably post it on erlang-bugs before opening a Jira, but either way would be fine.

Where Next?

Popular in Questions Top

New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: <h1>Create Post</h1> <%= ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31142 143
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement