Typespec and opts

Hi,

I’m trying to find examples of typespec usage with optional arguments. I assume the following is one way to write a spec with optional arguments:

@spec example1(String.t(), option1: String.t(), option2: integer()) :: String.t()
def example1(str, opts \\ [])

If I’d like to extract the options into a type because it is used in other places as well, would this be the correct way:

@type opts :: [option1: String.t(), option2: integer()]
@spec example1(String.t(), opts | nil) :: String.t()
def example1(str, opts \\ [])

Do I even need the | nil part?

Thanks,
Sebastian

1 Like

Both of your approaches require the elements of the option list to appear in the correct order, and also that all of them appear.

You probably want this:

@type opts :: {:option1, String.t()} | {:option2, integer()}
@spec example(String.t(), [opts]) :: String.t()
1 Like

Are you sure that all elements are required in that order in my first example? At least dialyzer is not picking this up with a straight up call to example1 with only a string passed. From what I understand this should be picked up?

Typespecs don’t have optional arguments. If you want to supply a typespec for both generated arities you need two specs:

@spec example1(String.t()) :: String.t()
@spec example1(String.t(), opts) :: String.t()
def example1(str, opts \\ []), do: …

Optional arguments do not mean they become nil when absent. Your function will become the following:

def example1(str), do: example1(str, [])
def example1(str, opts), do: …

There’s no implicit data being injected anywhere.

2 Likes

I am pretty sure that is not the case. There is no list type in the dialyzer typesystem that remembers the order of the internal types (or any requirement for an internal type to exist besides the general “nonempty”), and Elixir has special cased keyword lists in typespecs (https://hexdocs.pm/elixir/typespecs.html#literals, section “keyword list”), even though it looks a bit weird because no other list types have commas in them. Also it’s a bit strange that the documentation doesn’t mention that you can use more than one k/v types in the kwl special syntax, but you can… Maybe I should PR a fix to that doc.

Both of @Sgoettschkes examples are syntactically correct. 1 and 2 without the nil are semantically correct for what he’s trying to do. I would generally disfavor spec #1 because one doesn’t expect to unroll Elixir’s synatactic sugar inside of a typespec. It looks confusing and could be mistaken for an arity-3 function. #2 (without the nil) is very much idiomatic elixir; nobody really types out the version of the function with the default value, and I think that the dialyzer system is smart enough to deal with it correctly if you omit the lower-arity functions.

@NobbZ’s example is closest to what you would idiomatically see in erlang typespecs, and it’s the most flexible, since you can later do type magic and pull apart groups of options, for the purposes of documenting them separately, or applying them in separate cases if some functions take only a subset of the options.

3 Likes

You are indeed correct, my “order” argument isn’t valid. I got confused with pattern matching on keyword lists.

3 Likes

I think that’s also a good argument as to why you shouldn’t use form #1 even if it is legal and semantically correct!

Thanks for your comments. I decided to use the spec suggested by LostKobrakai:

@spec example1(String.t()) :: String.t()
@spec example1(String.t(), opts) :: String.t()
def example1(str, opts \\ []), do: …

I do think this makes it very clear how to call the function.

1 Like