Background
I am trying to use dialyzer to validate type specs in my code, but I wonder what is the best way of using it when we have functions that use pattern matching.
Code
Let’s focus on this code segment from Designing Elixir Systems with OTP (type specs added by me):
# Validator.check returns :ok if the 1st parameter is true and returns the second otherwise.
defp validate_generator({name, generator}) when is_atom(name) and is_list(generator), do:
Validator.check(generator != [], {:error, "can't be empty"})
defp validate_generator({name, generator}) when is_atom(name) and is_function(generator, 0), do:
:ok
defp validate_generator(_generate), do:
{:error, "must be a string to list or function pair"}
This code receives a tuple of an atom and a function and validates if the parameters have the correct format.
Dialyzing
There are 2 main ways of dialyzing this code:
@spec validate_generator(any) :: :ok | {:error, String.t}
defp validate_generator({name, generator}) when is_atom(name) and is_list(generator), do:
Validator.check(generator != [], {:error, "can't be empty"})
defp validate_generator({name, generator}) when is_atom(name) and is_function(generator, 0), do:
:ok
defp validate_generator(_generate), do:
{:error, "must be a string to list or function pair"}
This is a generic approach, but I find it too generalist. Although it is true that validate_generator
can receive anything as a parameter, if you actually pass it anything you will always end up in the 3rd pattern match, which is an error.
You also lack information about the other function clauses.
The other way of dialyzing this code would be the following:
@spec validate_generator({atom, list}) :: :ok | {:error, String.t}
defp validate_generator({name, generator}) when is_atom(name) and is_list(generator), do:
Validator.check(generator != [], {:error, "can't be empty"})
@spec validate_generator(atom, function) :: :ok
defp validate_generator({name, generator}) when is_atom(name) and is_function(generator, 0), do:
:ok
@spec validate_generator(any) :: {:error, "must be a string to list or function pair"}
defp validate_generator(_generate), do:
{:error, "must be a string to list or function pair"}
This alternative has the advantage of making it clear the types of parameters expected for each pattern match clause, although it is a little bit more verbose.
Questions
I would prefer the second way, however I am not sure if having 3 spec definitions for a single function would cause issues with dialyzer.
1.Wouldn’t the second approach confuse dialyzer (because I am defining the same spec 3 times)?
2. If the second approach has serious drawbacks, is there a way of improving the spec of the first approach?