在 Elixir 中使用类型规范中的原子列表

Use a list of atoms in type spec in Elixir

假设我有以下内容:

@some_list [:a, :b, :c, :d]

@type some_type :: :a | :b | :c | :d

是否有一些方法可以使用 @some_list 来定义 @type some_type 而无需明确使用 @some_list 中包含的相同原子?


编辑:为了清楚起见,我想将 @some_list 的内容重新用于 @type 构造,而 @some_list 仍可用于其他用途。

这在某种程度上可以通过一些元编程实现。

类型是特殊的,所以不能只在那里传递任意值并期望它在编译阶段被理解或扩展。

幸运的是,可以使用外部宏来定义模块类型属性。

请注意下面的代码不执行任何完整性检查,也不支持空列表和一个元素的列表

# inside module Helpers
defmacro one_of(name, list) do
  # attribute name must be an atom, type name we pass as is,
  #   hence we need to extract the atom name
  {attr_name, _, _} = name

  # reverse a list to iterate from the tail
  #   check what `quote do: :a | :b | :c` returns
  [last, prev | rest] =
    list
    |> Macro.expand(__CALLER__)
    |> Enum.reverse()

  # here we build the raw AST, 
  #   otherwise the compilation would fail
  type =
    Enum.reduce(rest, {:|, [], [prev, last]}, &{:|, [], [&1, &2]})

  quote do
    # declare the attribute
    Module.put_attribute(__MODULE__, unquote(attr_name), unquote(list))
    # declare the type
    @type unquote(name) :: unquote(type)
  end
end

现在我们可以从这个模块外部使用

require Helpers

Helpers.one_of(some_list, ~w|a b c d|a)

上面的调用类似于

@some_list ~w|a b c d|a
@type some_list :: :a | :b | :c | :d

我想到了以下内容,它几乎是在编译时从列表中逐字构造字符串 :a | :b | :c 并将其注入类型:

@some_list [:a, :b, :c]
@type t ::
        unquote(
          @some_list
          |> Enum.map(&inspect/1)
          |> Enum.join(" | ")
          |> Code.string_to_quoted!()
        )

它可能比 Aleksei Matiushkin 的解决方案更 hacky,但代码更少,如果是一次性的,可能更容易理解。