zachdaniel
Dynamically generate typespecs from module attribute list
I was hoping I could get something like this working:
defmodule FooRegistry do
@foos [
FirstFoo,
SecondFoo,
ThirdFoo
]
@foos_by_another_name Enum.into(foos, %{}, fn foo -> {foo.another_name(), foo} end)
@type foo [Enum.map(@foos, fn foo -> foo.t end)]
@spec list_foos() :: [foo]
def list_foos() do
@foos
end
@spec foo_from_another_name(atom) :: foo | nil
def foo_from_another_name(name) do
Map.get(@foos_by_another_name, name)
end
end
I’m not really worried about the exact syntax, but how might I go about dynamically generating a type like this?
Thanks!
Most Liked
evadne
For posterity:
defmodule Test do
@keys ~w(a b c)a
@type key :: unquote(Enum.reduce(@keys, &{:|, [], [&1, &2]}))
end
axelson
This is a great snippet! Here’s a function form of this:
defmodule TypeUtils do
# https://forum.elixirforum.net/t/dynamically-generate-typespecs-from-module-attribute-list/7078/5
def list_to_typespec(list) when is_list(list) do
Enum.reduce(list, &{:|, [], [&1, &2]})
end
end
Usage example:
@statuses [:not_started, :completed]
@type status :: unquote(TypeUtils.list_to_typespec(@statuses))
Eiji
In both @spec and @type you need to wrap everything in unquote call. Without that instead of calling function/macro the AST of such call would be stored:
iex> quote do
iex> union_type(@keys)
iex> end
{:union_type, [],
[{:@, [context: Elixir, import: Kernel], [{:keys, [context: Elixir], Elixir}]}]}
__using__/1 macro is as same as any other macro. use MyModule works like require MyModule + MyModule.__using__([]).
You can also write a macro like this one:
defmodule UnionType do
defmacro union_type({:"::", _, [{name, _, _}, data]}) do
quote bind_quoted: [data: data, name: name] do
@type unquote({name, [], Elixir}) :: unquote(UnionType.union_type_ast(data))
end
end
def union_type_ast([item]), do: item
def union_type_ast([head | tail]), do: {:|, [], [head, union_type_ast(tail)]}
end
defmodule Example do
import UnionType
@keys ~w(a b c)a
union_type key :: @keys
end
However for this you need to add this option:
[
# …
locals_without_parens: [union_type: 1]
]
to .formatter.exs, because otherwise it would format your code to:
defmodule Example do
import UnionType
@keys ~w(a b c)a
union_type(key :: @keys)
end
axelson
We’ve also now extracted this into a little library:
One of the main reasons to extract it was to avoid extra compile-time dependencies.
Eiji
@zachdaniel: You need to write helper module with some macros and then call them to generate ast for special module attributes like @type.
For @type you need to return ast in format:
# root element in case @foos count greater than 2:
{
:|,
[],
children
}
# root element in case @foos count is equal to 2:
{
:|,
[],
[FirstFoo.t, SecondFoo.t]
}
# root element in case @foos count is equal to 1:
FirstFoo.t
# root element in case @foos count is equal to 0:
raise error
Where for children you have same rules as for root element, so for 3 foos:
{
:|,
[],
[
FirstFoo.t,
{
:|,
[],
[
SecondFoo.t,
ThirdFoo.t
]
}
]
}
and this ast for you will look like:
FirstFoo.t | SecondFoo.t | ThirdFoo.t
What you need is to write recursive function that retruns Elixir AST in Tuple notation.







