Search code examples
typeselixirdialyzer

Elixir type specs and parameterized type variables


I am trying to figure out how to combine parameterized types and type variables in Elixir type and function specs. As a simple example, let's say I am defining a Stack module:

defmodule Stack do
  @type t :: t(any)
  @type t(value) :: list(value)

  @spec new() :: Stack.t
  def new() do
    []
  end

  # What should the spec be?
  def push(stack, item) do
    [item|stack]
  end
end

Using the parameterized type spec on line 3, I can define a function that creates a new stack that should contain only integers:

@spec new_int_stack() :: Stack.t(integer)
def new_int_stack(), do: Stack.new

So far, so good. Now I want to make sure that only integers can be pushed into this stack. For example, dialyzer should be fine with this:

int_stack = new_int_stack()
Stack.push(int_stack, 42)

But dialyzer should complain about this:

int_stack = new_int_stack()
Stack.push(int_stack, :boom)

I can't figure out what the type spec on the push function should be to enforce that. In Erlang, I'm pretty sure this syntax would do the trick:

-spec push(Stack, Value) -> Stack when Stack :: Stack.t(Value).

Is there a way to express this constraint using Elixir @spec?


Solution

  • (I am more fluent in plain Erlang, but the code should be easy to port.)

    If you write a separate int_push/2 (just as you did a new_int_stack/0) then you can of course write:

    -spec int_push(integer(), stack(integer())) -> stack(integer()).
    

    This should allow Dialyzer to detect abuses, purely due to the Item argument being specified to be an integer().

    The closest the generic spec can get is this:

    -spec push(T, stack(T)) -> stack(T) when T :: term().
    

    Unfortunately, as of Erlang 18, Dialyzer does not read this spec in the most strict sense (requiring all instances T to be unifiable). It just requires each T to be a term().

    Therefore, no warnings will be emitted in either Erlang or Elixir.

    Full code for an example in Erlang:

    -module(stack).
    
    -export([new/0, new_int_stack/0, push/2, test/0]).
    
    -type stack()  :: stack(any()).
    -type stack(T) :: list(T).
    
    -spec new() -> stack().
    
    new() ->
      [].
    
    -spec push(T, stack(T)) -> stack(T) when T :: term().
    
    push(Item, Stack) ->
      [Item|Stack].
    
    -spec new_int_stack() -> stack(integer()).
    
    new_int_stack() ->
      new().
    
    -spec test() -> ok.
    
    test() ->
      A = new(),
      B = new_int_stack(),
      push(foo, B),
      ok.