How can I create a macro that defines a type using atom that represents the type name? In the macro __type__ below, for the visibility argument :private, I’d like the macro to emit: @typep t() :: { ... }
def visibility(:public), do: :type
def visibility(:private), do: :typep
def visibility(:opaque), do: :opaque
defmacro __type__(types, visibility) do
attr = visibility(visibility)
quote bind_quoted: [attr: attr, types: types] do
Module.put_attribute(__MODULE__, unquote(attr), t() :: {unquote_splicing(types)}
end
end
Compilation fails with:
== Compilation error in file test/type_test.exs ==
** (CompileError) test/type_test.exs:49: misplaced operator ::/2
The :: operator is typically used in bitstrings to specify types and sizes of segments:
<<size::32-integer, letter::utf8, rest::binary>>
It is also used in typespecs, such as @type and @spec, to describe inputs and outputs
... expanding macro: Mod.__type__/2
defmacro __type__(types, visibility) do
attr = Mod.visibility(visibility)
quote bind_quoted: [attr: attr, types: types] do
@unquote(attr)(t() :: {unquote_splicing(types)})
end
end
For this invocation I get:
== Compilation error in file test/type_test.exs ==
** (CompileError) test/type_test.exs:79: unquote called outside quote
(elixir 1.13.2) expanding macro: Kernel.@/1
defmodule Example do
defmacro sample(ast, visibility) do
# sample visibility handling
attr =
case visibility do
:public -> :type
:private -> :typep
end
# this optional line allows you to use sigils:
# Example.sample(~w[a b c]a, :private)
list = Macro.expand(ast, __CALLER__)
# if unuoqte needs to be used which is the case here
# then quote cannot have bind_quoted option set
# as this option sets unquote option to false
# making unquote/1 calls not work
quote do
# @ is a macro which means we need to use unquote here
# to pass a raw data instead of ast not recognized by said macro
@unquote(attr)(t() :: {unquote_splicing(list)})
end
end
end
How would you rewrite it if the first argument to your sample/2 macro is actually the content of a dynamically built attribute? E.g.
def prep_sample() do
quote do
Module.register_attribute(__MODULE__, :types, accumulate: true)
end
Module.put_attribute(mod, :types, {:a, String.t()})
Module.put_attribute(mod, :types, {:b, atom()})
Module.put_attribute(mod, :types, {:c, integer()})
end
And I essentially want to make the type containing the types taken from the types attribute in the “pseudo” code below:
defmacro sample(types, visibility) do
attr =
case visibility do
:public -> :type
:private -> :typep
end
list = for {_,type} <- types, do: type
quote do
@unquote(attr)(t() :: {unquote_splicing(list)})
end
end
def define_type(visibility) do
prep_sample()
sample(Module.get_attribute(__MODULE__, :types), visibility)
end
Your code would not even compile. Also you can put module attributes only when mod is does not yet finished compiling. quote/1 inside a function should be returned (as last expression) when said function is called by macro. Otherwise in function body you would have a quoted expression which would not be evaluated.
Maybe you look for something like this?
defmodule Example do
defmacro __using__(_opts \\ []) do
quote do
import Example, only: [type: 2, using: 1, using: 2]
Module.register_attribute(__MODULE__, :_types, accumulate: true)
end
end
defmacro type(name, ast) do
quote bind_quoted: [ast: Macro.escape(ast), name: name] do
@_types {name, ast}
end
end
defmacro using(opts \\ [visibility: :public], do: block) do
attr = visibility(opts[:visibility])
quote bind_quoted: [attr: attr, block: block] do
_ = block
list = @_types |> Enum.map(&elem(&1, 1)) |> Enum.reverse()
case attr do
:opaque -> @opaque t() :: {unquote_splicing(list)}
:type -> @type t() :: {unquote_splicing(list)}
:typep -> @typep t() :: {unquote_splicing(list)}
end
end
end
defp visibility(:opaque), do: :opaque
defp visibility(:private), do: :typep
defp visibility(:public), do: :type
end
defmodule Test do
use Example
using visibility: :private do
type :a, String.t()
type :b, atom()
type :c, integer()
end
end
Thanks Eiji! What prompted this question was my modification of the typed_struct library that adds support of typedrecord, e.g.:
defmodule Person do
use TypedStruct
typedrecord :person do
@typedoc "A person"
field :name, String.t(),
field :age, non_neg_integer(), default: 0
end
end
The code above will be expanded to:
defmodule Person do
use Record
Record.defrecord(:person, name: nil, age: 0)
@type person :: {:person, String.t, non_neg_integer}
end
There were two places in my implementation that I wanted to improve: this and this to avoid repetition.