Use a list of atoms in type spec in Elixir

Viewed 751

Assume I have the following:

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

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

Is there some way to use @some_list to define @type some_type without explicitly using the same atoms contained in @some_list?


EDIT: Just to make it clear, I would like to reuse the content of @some_list for the @type construct, while @some_list shall remain available of other uses.

2 Answers

I came up with the following, which almost literally constructs the string :a | :b | :c from the list at compile time and injects it into the type:

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

It's maybe a bit more hacky than Aleksei Matiushkin's solution, but has less code and may be more understandable if it's a one-off.

This is possible to some extent with a bit of metaprogramming.

Types are special, so one cannot just pass the arbitrary value there and expect it to be understood or expanded during the compilation stage.

Luckily enough, one might use an external macro to define both module and type attributes.

Please note the code below does not perform any sanity check neither does support empty lists and lists of one element

# 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

Now we can use from outside of this module

require Helpers

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

The call above is similar to

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