Elixir 透析器的多态类型

Polymorphic types for dialyzer in Elixir

背景

我有一个名为 MyApp.Resultstruct,它基本上是 Result Monad 的表示。该结构旨在成为操作成功和错误的正式结构表示:

defmodule MyApp.Result do

  @enforce_keys [:type]
  defstruct type: nil,
            result: nil,
            error_reason: nil,
            error_details: nil,
            input_parameters: []

  
  @type type :: :ok | :error
  @type result :: any()
  @type reason :: atom() | nil
  @type details :: any()
  @type params :: [any()]

  @type t() :: %__MODULE__{
          type: type(),
          result: result(),
          error_reason: reason(),
          error_details: details(),
          input_parameters: params()
        }

  @spec ok :: __MODULE__.t()
  def ok, do: %__MODULE__{type: :ok}


  @spec ok(result()) :: __MODULE__.t()
  def ok(result), do: %__MODULE__{type: :ok, result: result}

  @spec error(reason()) :: __MODULE__.t()
  def error(reason), do: %__MODULE__{type: :error, error_reason: reason}

  @spec error(reason(), details()) :: __MODULE__.t()
  def error(reason, details) do
    %__MODULE__{type: :error, error_reason: reason, error_details: details}
  end

  @spec error(reason(), details(), params()) :: __MODULE__.t()
  def error(reason, details, input) do
    %__MODULE__{
      type: :error,
      error_reason: reason,
      error_details: details,
      input_parameters: input
    }
  end

问题

在向我的一位同事展示这个时,他提出了一个很好的观点:

I see what you are trying to do here, but when I see a MyApp.Result struct I have to check the code to know what is inside of the result field in case of success. For me this is just another layer that hides things and it does not make clear what the operation returns if it succeeds.

公平地说,我认为这是一个很好的观点。结果 Monad 隐藏计算结果,直到你需要它们。但我确实认为有更好的方法,一种我们仍然可以拥有明确显示结果字段类型的 Result Monad 的方法。

透析器的多态类型

我相信我的问题的解决方案可能是透析器的多态类型。引用一篇文章:

From Learn You Some Erlang When I said we could define a list of integers as [integer()] or list(integer()), those were polymorphic types. It's a type that accepts a type as an argument.

To make our queue accept only integers or cards, we could have defined its type as:

-type queue(Type) :: {fifo, list(Type), list(Type)}.
-export_type([queue/1]).

所以现在我知道这在 erlang 中是可能的。如果在那里可能,在 Elixir 中也应该是可能的。

错误

所以我将代码更改为以下内容:

@enforce_keys [:type]
  defstruct type: nil,
            result: nil,
            error_reason: nil,
            error_details: nil,
            input_parameters: []

  @type type :: :ok | :error
  @type result :: Type
  @type reason :: atom() | nil
  @type details :: any()
  @type params :: [any()]

  @type t(Type) :: %__MODULE__{
          type: type(),
          result: result(),
          error_reason: reason(),
          error_details: details(),
          input_parameters: params()
        }

  @spec ok :: BusyBee.Wrappers.FFmpeg.Result.t(nil)
  @spec ok :: __MODULE__.t()
  def ok, do: %__MODULE__{type: :ok}

  @spec ok(result()) :: __MODULE__.t(Type)
  def ok(result), do: %__MODULE__{type: :ok, result: result}

然而,这打破了。我不知道如何使用透析器在 Elixir 中表示多态类型。

问题

如何修复此代码,以便我的透析器知道 Result 是多态类型?

中,变量的命名以大写字母开头(-type queue(Type) :: ….)

%% ERLANG
Foo = math:pow(10, 2).

中,相反,它们是以小写字母开头的。大写名称 (Type) 为特殊原子保留,由编译器转换为 :"Elixir.Type" 个原子。

# ELIXIR
foo = :math.pow(10, 2)

也就是说,多态类型可以声明为

@type foo(t) :: [t]

示例来自 : https://github.com/elixir-ecto/ecto/blob/v3.6.2/lib/ecto/multi.ex#L124