Help with dialyzer warning on typespec that includes anonymous function

I have the following small bind (or flat_map) implementation for error tuples.

@spec bind({:ok, a} | {:error, reason}, function(a) :: {:ok, b} | {:error, reason}) ::
        {:ok, b} | {:error, reason}
      when a: any, b: any, reason: term
def bind({:ok, value}, func) when is_function(func, 1), do: func.(value)
def bind({:error, reason}, _func), do: {:error, reason}

When I use this function as follows

@spec safe_div(integer, integer) :: {:ok, float} | {:error, :zero_division}
def safe_div(_, 0) do
  {:error, :zero_division}
end

def safe_div(a, b) do
  {:ok, a / b}
end

OK.bind({:ok, 4}, &safe_div(3, &1))

I see the following warning.

test/integration.ex:5:no_return
Function run/0 has no local return.
________________________________________________________________________________
Please file a bug in https://github.com/jeremyjh/dialyxir/issues with this message.

Failed to parse warning:
[{:"(", 1}, {:"{", 1}, {:atom_full, 1, '\'ok\''}, {:",", 1}, {:atom_part, 1, 'a'}, {:"}", 1}, {:|, 1}, {:"{", 1}, {:atom_full, 1, '\'error\''}, {:",", 1}, {:atom_part, 1, 'r'}, {:atom_part, 1, 'e'}, {:atom_part, 1, 'a'}, {:atom_part, 1, 's'}, {:atom_part, 1, 'o'}, {:atom_part, 1, 'n'}, {:"}", 1}, {:",", 1}, {:atom_part, 1, 'f'}, {:atom_part, 1, 'u'}, {:atom_part, 1, 'n'}, {:atom_part, 1, 'c'}, {:atom_part, 1, 't'}, {:atom_part, 1, 'i'}, {:atom_part, 1, 'o'}, {:atom_part, 1, 'n'}, {:"(", 1}, {:atom_part, 1, 'a'}, {:")", 1}, {:::, 1}, {:"{", 1}, {:atom_full, 1, '\'ok\''}, {:",", 1}, {:atom_part, 1, 'b'}, {:"}", 1}, {:|, 1}, {:"{", 1}, {:atom_full, 1, '\'error\''}, {:",", 1}, {:atom_part, 1, 'r'}, {:atom_part, 1, 'e'}, {:atom_part, 1, 'a'}, {:atom_part, 1, 's'}, {:atom_part, 1, 'o'}, {:atom_part, 1, 'n'}, {:"}", 1}, {:")", 1}, {:->, 1}, {:"{", ...}, {...}, ...]


Legacy warning:
test/integration.ex:12: The call 'Elixir.OK':bind({'ok', 4},fun((_) -> {'error','zero_division'} | {'ok',float()})) breaks the contract ({'ok',a} | {'error',reason},function(a)::{'ok',b} | {'error',reason}) -> {'ok',b} | {'error',reason} when a :: any(), b :: any(), reason :: term()
________________________________________________________________________________
test/integration.ex:42:unused_fun
Function fetch_key/2 will never be called.
________________________________________________________________________________
done (warnings were emitted)

I can’t make head or tail of it. Any help would be appreciated.
You can see the full code on this PR. https://github.com/CrowdHailer/OK/pull/54

You have the function type wrong. I’ll try to take a closer look into it in a couple of minutes.

To specify a lambda, you should use (arg1, arg2, ... -> result). With that change, the following typespec should be working:

@spec bind({:ok, a} | {:error, reason}, (a -> {:ok, b} | {:error, reason})) ::
          {:ok, b} | {:error, reason}
        when a: any, b: any, reason: term
3 Likes

Yes exactly, that was my intuition as well, but at the time of my writing, I was only on a mobile, the following diff works for me:

diff --git a/lib/ok.ex b/lib/ok.ex
index dd333dc..b640eb0 100644
--- a/lib/ok.ex
+++ b/lib/ok.ex
@@ -44,3 +44,3 @@ defmodule OK do
   """
-  @spec bind({:ok, a} | {:error, reason}, function(a) :: {:ok, b} | {:error, reason}) ::
+  @spec bind({:ok, a} | {:error, reason}, (a -> {:ok, b} | {:error, reason})) ::
           {:ok, b} | {:error, reason}

Interesting. I wonder why this one was not a problem.

Because you create a parameter named function(a) which shall have the type b, which again is an alias for any. And there is no value that contradicts any.

2 Likes

Thanks.

One further question.
The parameters in the spec (a, b, reason) are just alias for anyway?
There is no way to make dialyzer realize that if given a function that returns integers the return type will be {:ok, integer} is there?

i.e.

{:ok, :my_atom} = OK.bind({:ok, 4}, &safe_div(3, &1))

That should raise an error becase the return type is really {:ok, integer} | {:error, term}. As far as I am aware there is no way to teach dialyzer that

That all occurrences of a should have the same type is only a semantic and mostly of documentation also purpose. Dialyzer does not check if this is actually true.