0

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'
Parth Shah
  • 2,060
  • 1
  • 23
  • 34

1 Answers1

1

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) 
7stud
  • 46,922
  • 14
  • 101
  • 127
  • “I don't see any elixir typespec tutorials [...]”— http://erlang.org/doc/reference_manual/typespec.html#specifications-for-functions – Aleksei Matiushkin Jun 17 '19 at 05:40
  • Also, please, use the proper formatting when suggesting anything: types are _functions_ under the hood, so they require parentheses according to Elixir guidelines (`CustomType.t()` not `CustomType.t`.) – Aleksei Matiushkin Jun 17 '19 at 05:42
  • Is there a reason why the first calculate doesn’t have <> but the second one does? – Parth Shah Jun 17 '19 at 07:29
  • @ParthShah, I just wanted to show you another way to make sure an arg is a binary. – 7stud Jun 17 '19 at 22:18
  • @AlekseiMatiushkin, Yet, elixir-lang.org [doesn't use parens](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html) – 7stud Jun 17 '19 at 22:22
  • This is legacy originated to pre-[formatter](https://elixir-lang.org/blog/2018/01/17/elixir-v1-6-0-released/) era. Since 1.6 it’s suggested to use `formatter` / `mix format` on your codebase, which appends parentheses everywhere. – Aleksei Matiushkin Jun 18 '19 at 05:18
  • @7stud I added a sample CustomType and updated the functions in the question to match exactly what I have. Can you please see if you can repro the dialyzer output? – Parth Shah Jun 18 '19 at 05:58
  • @ParthShah, Your specs do not specify a return type, and no one can run an example that has `...` in it. Like I said, you have to post a minimal example that will duplicate the dialyzer output. – 7stud Jun 18 '19 at 16:31
  • Sorry about that. I didn't feel that mattered. I will update with a full sample. – Parth Shah Jun 18 '19 at 16:38
  • @ParthShah, *I didn't feel that mattered.* -- My example produces no dialyzer warnings, right? I filled in the `...` myself. – 7stud Jun 18 '19 at 17:05
  • I changed the function definition. Not sure if you saw this. The second calculate() does pattern matching on selected members of the struct instead of the whole struct. I don’t see that in your example. – Parth Shah Jun 18 '19 at 17:07
  • @ParthShah, No, I didn't see that. I'll take a look at it. – 7stud Jun 18 '19 at 17:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/195147/discussion-between-parth-shah-and-7stud). – Parth Shah Jun 18 '19 at 17:51
  • @ParthShah, I can't duplicate your dialyzer output, but once again I had to fill in stuff that you neglected. You need to post a full example that anyone can run without error and which produces the dialyzer warning. – 7stud Jun 19 '19 at 00:57
  • @ParthShah, Or let's try it this way: run the code I posted at the bottom of my answer. What does dialyzer say? – 7stud Jun 19 '19 at 00:58
  • Can you try adding the same dialyzer flags that I have mentioned at the end of my question? – Parth Shah Jun 19 '19 at 01:03
  • @ParthShah, See the end of my answer. – 7stud Jun 19 '19 at 01:12
  • I updated my question with a code snippet that looks identical to yours, however, I am getting dialyzer warnings for mine but not for yours. I am so confused! – Parth Shah Jun 20 '19 at 05:16