5

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?

4

1 回答 1

8

(我对普通的 Erlang 更流利,但代码应该很容易移植。)

如果你单独写一个int_push/2(就像你写一个new_int_stack/0),那么你当然可以写:

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

这应该允许 Dialyzer 检测滥用行为,纯粹是因为Item参数被指定为integer().

通用规范可以得到的最接近的是:

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

不幸的是,从 Erlang 18 开始,Dialyzer 并没有在最严格的意义上阅读这个规范(要求所有实例T都是统一的)。它只要求每个T都是term().

因此,在 Erlang 或 Elixir 中都不会发出警告。

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.
于 2015-11-03T09:16:48.630 回答