Dialyzer emits Type specification is a subtype of the success typing for multiple functions with same name

Parth Shah picture Parth Shah · Jun 17, 2019 · Viewed 9.2k times · Source

I have the following two functions and I am receiving this dialyzer warning on them:

"Type specification 'Elixir.MyModule':calculate(arg1::'Elixir.String':t(),arg2::'Elixir.CustomType':t(),arg3::'Elixir.String':t()) -> 'ok' | 'error'; calculate(arg1::'Elixir.String':t(),arg2::'Elixir.CustomType':t(),arg3::maybe_improper_list()) -> 'ok' | 'error' is a subtype of the success typing: 'Elixir.MyModule':calculate(binary(),#{'__struct__':='Elixir.CustomType', _=>_},binary() | maybe_improper_list()) -> 'error' | 'ok'"

Here are the functions:

@spec calculate(arg1 :: String.t, arg2 :: CustomType.t, arg3 :: String.t)
def calculate(arg1, %CustomType{} = arg2, arg3) when is_binary(arg1) and is_binary(arg3) do
  calculate(arg1, arg2, [arg3])
end

@spec calculate(arg1 :: String.t, arg2 :: CustomType.t, arg3 :: maybe_improper_list())
def calculate(arg1, %CustomType{prop1: val, prop2: val2}, arg3) when is_binary(arg1) and is_integer(val2) and is_binary(val) and is_list(arg3) do
...
end

I don't understand why I am getting this warning. I thought this is the correct way to write functions with different argument types in Elixir but given Dialyzer keeps emitting warnings, I am beginning to wonder if I am writing this code incorrectly?


defmodule CustomType do
  @type t :: %CustomType{
    prop1: String.t(),
    prop2: integer(),
    prop3: String.t(),
    prop4: boolean(),
    ...
  }
end

These are the dialyzer flags I am running with:

dialyzer: [
  flags: ~w[underspecs overspecs race_conditions error_handling unmatched_returns]a
]

Repro Sample:

defmodule MyCustomType do
    @type t :: %MyCustomType{
        prop1: String.t(),
        prop2: integer(),
        prop3: String.t()
    }

    defstruct [:prop1, :prop2, :prop3]
end

defmodule MyModule do
    @spec calculate(String.t, 
                    MyCustomType.t, 
                    String.t) 
    :: :ok
    def calculate(arg1, %MyCustomType{} = arg2, arg3) when is_binary(arg1) and is_binary(arg3) do
        calculate(arg1, arg2, [arg3])
    end

    @spec calculate(String.t, 
                    MyCustomType.t, 
                    maybe_improper_list) 
    :: :ok
    def calculate(arg1, %MyCustomType{prop1: val, prop2: val2}, arg3) when is_binary(arg1) and is_list(arg3) and is_binary(val) and is_integer(val2) do
        :ok
    end
end

Here are the warnings I am getting:

      Type specification 'Elixir.MyModule':calculate
          ('Elixir.String':t(),
          'Elixir.MyCustomType':t(),
          'Elixir.String':t()) ->
             'ok';
         ('Elixir.String':t(),
          'Elixir.MyCustomType':t(),
          maybe_improper_list()) ->
             'ok' is a subtype of the success typing: 'Elixir.MyModule':calculate
          (binary(),
          #{'__struct__' := 'Elixir.MyCustomType',
            'prop1' := binary(),
            'prop2' := integer(),
            _ => _},
          binary() | maybe_improper_list()) ->
             'ok'

Answer

7stud picture 7stud · Jun 17, 2019

First, I don't see any elixir typespec tutorials where they write the @spec with the variable names in the spec--instead all I find are tutorials with types only in the typespec:

@spec calculate(arg1 :: String.t, arg2 :: CustomType.t, arg3 :: String.t)

v.

@spec calculate(String.t, CustomType.t, String.t)

Nevertheless, the following passed dialyzer for me:

defmodule CustomType do
  @type t :: %CustomType{}
  defstruct a: nil, b: nil
end

defmodule MyModule do

  @spec calculate(arg1::String.t, arg2::CustomType.t, arg3::String.t) :: number

  #def calculate(<<arg1::binary>>, %CustomType{} = arg2, <<arg3::binary>>) do
  def calculate(arg1, %CustomType{} = arg2, arg3) when is_binary(arg1) and is_binary(arg3) do
    calculate(arg1, arg2, [arg3])
  end

  @spec calculate(String.t, CustomType.t, maybe_improper_list()) :: number

  def calculate(<<arg1::binary>>, %CustomType{} = arg2, arg3) when is_list(arg3) do
    123
  end

end

~/elixir_programs/friends$ mix dialyzer
Compiling 1 file (.ex)
warning: variable "arg1" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/friends/my_module.ex:17

warning: variable "arg2" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/friends/my_module.ex:17

Checking PLT...
[:compiler, :connection, :crypto, :db_connection, :decimal, :ecto, :elixir,
 :kernel, :logger, :poolboy, :postgrex, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [
  check_plt: false,
  init_plt: '/Users/7stud/elixir_programs/friends/_build/dev/dialyxir_erlang-20.3_elixir-1.8.2_deps-dev.plt',
  files_rec: ['/Users/7stud/elixir_programs/friends/_build/dev/lib/friends/ebin'],
  warnings: [:unknown]
]
done in 0m1.43s
done (passed successfully)

I will say that I find this syntax:

@spec calculate(String.t, CustomType.t, String.t)

much easier to read.

According to Learn You Some Erlang:

is a subtype of the success typing

This warns you that in fact, your specification is way too strict for what your code is expected to accept, and tells you (albeit indirectly) that you should either make your type specification looser, or validate your inputs and outputs better in your functions to reflect the type specification.

However, I am unable to produce your dialyzer output:

defmodule CustomType do
  @type t :: %CustomType{}
  defstruct a: nil, b: nil
end

defmodule MyModule do

  @spec calculate(arg1 :: String.t, 
                  arg2 :: CustomType.t, 
                  arg3 :: String.t) 
  :: :ok | :error

  def calculate(arg1, %CustomType{} = arg2, arg3) 
  when is_binary(arg1) and is_binary(arg3) do
    calculate(arg1, arg2, [arg3])
  end

  @spec calculate(arg1 :: String.t, 
                  arg2 :: CustomType.t, 
                  arg3 :: maybe_improper_list()) 
  :: :ok | :error

  def calculate(arg1, %CustomType{} = arg2, arg3) 
  when is_binary(arg1) and is_list(arg3) do
    case arg1 do
      "hello" -> :ok
      "goodbye"  -> :error
    end
  end

end

$ mix dialyzer
Compiling 1 file (.ex)
warning: variable "arg2" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/friends/my_module.ex:19

Checking PLT...
[:compiler, :connection, :crypto, :db_connection, :decimal, :ecto, :elixir,
 :kernel, :logger, :poolboy, :postgrex, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [
  check_plt: false,
  init_plt: '/Users/7stud/elixir_programs/friends/_build/dev/dialyxir_erlang-20.3_elixir-1.8.2_deps-dev.plt',
  files_rec: ['/Users/7stud/elixir_programs/friends/_build/dev/lib/friends/ebin'],
  warnings: [:unknown]
]
done in 0m1.46s
done (passed successfully)

So, you need to post a minimal example that reproduces your dialyzer output. I will note that arg3 has to be a binary in your first clause, so when you call calculate(arg1, arg2, [arg3]) in the first clause's body, the argument [arg3] will never be an improper list, so you can tighten that spec up to: list(binary) for the second clause.

============

Here's the code I've put together:

defmodule CustomType do
  @type t :: %CustomType {
    prop1: String.t(),
    prop2: integer(),
    prop3: String.t(),
    prop4: boolean()
  }

  defstruct prop1: nil, prop2: nil, prop3: nil, prop4: nil
end

defmodule MyModule do
  @spec calculate(arg1 :: String.t, 
                  arg2 :: CustomType.t, 
                  arg3 :: String.t) 
  :: :ok | :error

  def calculate(arg1, %CustomType{} = arg2, arg3) 
  when is_binary(arg1) and is_binary(arg3) do
    calculate(arg1, arg2, [arg3])
  end

  @spec calculate(arg1 :: String.t, 
                  arg2 :: CustomType.t, 
                  arg3 :: maybe_improper_list) 
  :: :ok | :error

  def calculate(arg1, %CustomType{prop1: val, prop2: val2}, arg3) 
  when is_binary(arg1) and is_binary(val) and is_integer(val2) and is_list(arg3) do
    case arg1 do
      "hello" -> :ok
      "goodbye"  -> :error
    end
  end

end

Running dialyzer:

~/elixir_programs/friends$ mix dialyzer 
Checking PLT... [:artificery,  
 :compiler, :connection, :crypto, :db_connection, :decimal,  :distillery,  
 :ecto, :elixir, :kernel, :logger, :poolboy, :postgrex,  :runtime_tools,  
 :stdlib] Finding suitable PLTs Looking up modules in dialyxir_erlang-  
20.3_elixir-1.8.2_deps-dev.plt Finding applications for dialyxir_erlang-  
20.3_elixir-1.8.2_deps-dev.plt Finding modules for dialyxir_erlang-  
20.3_elixir-1.8.2_deps-dev.plt Checking 718 modules in dialyxir_erlang-  
20.3_elixir-1.8.2_deps-dev.plt Adding 56 modules to dialyxir_erlang-  
20.3_elixir-1.8.2_deps-dev.plt Starting Dialyzer dialyzer args: [   
  check_plt: false,   
init_plt: '/Users/7stud/elixir_programs/friends/_build/dev/dialyxir_erlang-  
20.3_elixir-1.8.2_deps-dev.plt', files_rec:   
['/Users/7stud/elixir_programs/friends/_build/dev/lib/friends/ebin'],  
  warnings: [:unknown] ] done in 0m1.26s done (passed successfully)  

With the following in mix.exs:

  def project do
    [
      app: :friends,
      version: "0.1.0",
      elixir: "~> 1.6",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      dialyzer: [
            flags: ~w[underspecs 
                      overspecs 
                      race_conditions
                      error_handling 
                      unmatched_returns]a
      ]
    ]
  end

Here's the output:

~/elixir_programs/friends$ mix dialyzer
Checking PLT...
[:artificery, :compiler, :connection, :crypto, :db_connection, :decimal,  
:distillery, :ecto, :elixir, :kernel, :logger, :poolboy, :postgrex,  
:runtime_tools, :stdlib]  
PLT is up to date!  
Starting Dialyzer  
dialyzer args: [
  check_plt: false,
  init_plt: '/Users/7stud/elixir_programs/friends/_build/dev/dialyxir_erlang-20.3_elixir-1.8.2_deps-dev.plt',
  files_rec: ['/Users/7stud/elixir_programs/friends/_build/dev/lib/friends/ebin'],
  warnings: [:underspecs, :overspecs, :race_conditions, :error_handling,
   :unmatched_returns, :unknown]
]
done in 0m1.38s  
done (passed successfully)