Help with a typed struct macro

I’m trying to write my own typed struct macro. It feels like it’s almost working, but I have a little snag with %__MODULE__{} that I can’t seem to get over.

Here is the macro:

defmodule TypedStruct do
  defmacro deftypedstruct(fields, enforced_fields \\ nil) do
    fields_map =
      case fields do
        {:%{}, _, flist} -> Enum.into(flist, %{})
        _ -> raise ArgumentError, "Fields must be a map!"
      end

    enforced_list = if is_nil(enforced_fields), do: Map.keys(fields_map), else: enforced_fields
    field_specs = Map.to_list(fields_map)
    field_names = Map.keys(fields_map)

    IO.inspect(field_specs)
    IO.inspect(enforced_list)
    IO.inspect(field_names)

    quote do
      @type t :: %__MODULE__{unquote(field_specs)}
      @enforce_keys unquote(enforced_list)
      defstruct unquote(field_names)
    end
  end
end

And here is how I’m using it:

defmodule Foo do
  require TypedStruct
  TypedStruct.deftypedstruct(%{foo: String.t(), bar: nil | integer()}, [:foo])
end

The printout when running that is:

➜  ~ elixir -r typedstruct.ex test.ex
[
  bar: {:|, [line: 3], [nil, {:integer, [line: 3], []}]},
  foo: {{:., [line: 3], [{:__aliases__, [line: 3], [:String]}, :t]}, [line: 3],
   []}
]
[:foo]
[:bar, :foo]
** (CompileError) test.ex:3: expected key-value pairs in struct __MODULE__
    (elixir 1.10.2) lib/kernel/typespec.ex:898: Kernel.Typespec.compile_error/2
    (elixir 1.10.2) lib/kernel/typespec.ex:294: Kernel.Typespec.translate_type/2
    (stdlib 3.11.2) lists.erl:1354: :lists.mapfoldl/3
    (elixir 1.10.2) lib/kernel/typespec.ex:228: Kernel.Typespec.translate_typespecs_for_module/2
    (elixir 1.10.2) src/elixir_erl_compiler.erl:12: anonymous fn/3 in :elixir_erl_compiler.spawn/2

You can see my field_specs is a keyword list of field name and field specs (in quoted form). But unquoting it to the %__MODULE__{} is not working for some reason. I can’t seem to find a correct combo of unquote/escape that works. Any help would be good, even better if you can make me understand the issue! :smiley:

@Nicd It’s really simple … You cannot write:

%SomeModule{[some_field: integer()]}

instead you should write:

%SomeModule{some_field: integer()}

To do that in macro instead of unquote/1 you need to use unquote_splicing/1.

Unquotes the given list expanding its arguments.

Similar to unquote/1 .

Examples

iex> values = [2, 3, 4]
iex> quote do
...>   sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]}

Source: Kernel.SpecialForms — Elixir v1.16.0

4 Likes

That was it, thank you! :heart: