Imaginons une implémentation de module Scaffold
qui génÚre une structure avec des champs personnalisés prédéfinis et l'injecte dans un module appelé en utilisant use Scaffold
. Lorsqu'il est appelé use Scaffold, fields: foo: [custom_type()], ...
, nous voulons implémenter le type correct dans le Consumer
module ( common_field
dans l'exemple ci-dessous, il est défini dans Scaffold
ou ailleurs de l'extérieur).
@type t :: %Consumer{
common_field: [atom()],
foo: [custom_type()],
...
}
Ce serait cool si nous pouvions générer avec précision le type Consumer.t()
pour une utilisation future et créer une documentation appropriée pour les utilisateurs de notre nouveau module.
Un exemple plus compliqué ressemblerait à ceci:
defmodule Scaffold do
defmacro __using__(opts) do
quote do
@fields unquote(opts[:fields])
@type t :: %__MODULE__{
version: atom()
# magic
}
defstruct @fields
end
end
end
defmodule Consumer do
use Scaffold, fields: [foo: integer(), bar: binary()]
end
et, aprĂšs compilation:
defmodule Consumer do
@type t :: %Consumer{
version: atom(),
foo: integer(),
bar: binary()
}
defstruct ~w|version foo bar|a
end
Ăa a l'air facile, non?
Approche naĂŻve
Commençons par analyser dans quel AST nous entrons Scaffold.__using__/1
.
defmacro __using__(opts) do
IO.inspect(opts)
end
#â [fields: [foo: {:integer, [line: 2], []},
# bar: {:binary, [line: 2], []}]]
Excellent. Il semble que nous soyons Ă un pas du succĂšs.
quote do
custom_types = unquote(opts[:fields])
...
end
#â == Compilation error in file lib/consumer.ex ==
# ** (CompileError) lib/consumer.ex:2: undefined function integer/0
Bams! Les types sont quelque chose de spĂ©cial, comme on dit dans la rĂ©gion de Privoz; nous ne pouvons pas simplement les prendre et les obtenir de l'AST partout. Peut-ĂȘtre que cela unquote
fonctionnera localement?
@type t :: %__MODULE__{
unquote_splicing([{:version, atom()} | opts[:fields]])
}
#â == Compilation error in file lib/scaffold.ex ==
# ** (CompileError) lib/scaffold.ex:11: undefined function atom/0
Peu importe comment c'est. Les types sont fatigants; demandez à quiconque gagne sa vie avec un Haskell (et ce sont les types de fumeurs à Haskell; les vrais - dépendants - sont cent fois plus utiles, mais deux cents fois plus difficiles).
, , AST , , .
AST
, , . , , , - . , . , , AST ( unquote
binary()
, CompileError
.
, quote do
, , quote
, â AST.
quote do
Enum.map([:foo, :bar], & &1)
end
#â {
# {:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
# [[:foo, :bar], {:&, [], [{:&, [], [1]}]}]}
? , AST, Enum
, :map
, . , AST quote
. .
, AST AST, . ? â , .
defmacro __using__(opts) do
fields = opts[:fields]
keys = Keyword.keys(fields)
type = ???
quote location: :keep do
@type t :: unquote(type)
defstruct unquote(keys)
end
end
, , â AST, . , ruby !
iex|1 quote do
...|1 %Foo{version: atom(), foo: binary()}
...|1 end
#â {:%, [],
# [
# {:__aliases__, [alias: false], [:Foo]},
# {:%{}, [], [version: {:atom, [], []}, foo: {:binary, [], []}]}
# ]}
?
iex|2 quote do
...|2 %{__struct__: Foo, version: atom(), foo: binary()}
...|2 end
#â {:%{}, [],
# [
# __struct__: {:__aliases__, [alias: false], [:Foo]},
# version: {:atom, [], []},
# foo: {:binary, [], []}
# ]}
, , . .
defmacro __using__(opts) do
fields = opts[:fields]
keys = Keyword.keys(fields)
type =
{:%{}, [],
[
{:__struct__, {:__MODULE__, [], ruby}},
{:version, {:atom, [], []}}
| fields
]}
quote location: :keep do
@type t :: unquote(type)
defstruct unquote(keys)
end
end
, Scaffold
, ( : Qqwy here). , , version: atom()
quote
.
defmacro __using__(opts) do
fields = opts[:fields]
keys = Keyword.keys(fields)
fields_with_struct_name = [__struct__: __CALLER__.module] ++ fields
quote location: :keep do
@type t :: %{unquote_splicing(fields_with_struct)}
defstruct unquote(keys)
end
end
(mix docs
):
: AST
, AST __using__/1
, ? , unquote
quote
? , , . , .
NB ,atom()
, , ,GenServer.on_start()
. .
, quote do
, - atom()
( CompileError
, ). , - :
keys = Keyword.keys(fields)
type =
{:%{}, [],
[
{:__struct__, {:__MODULE__, [], ruby}},
{:version, {:atom, [], []}}
| Enum.zip(keys, Stream.cycle([{:atom, [], []}]))
]}
, @type
? Quoted Fragment, :
defmodule Squares do
Enum.each(1..42, fn i ->
def unquote(:"squared_#{i}")(),
do: unquote(i) * unquote(i)
end)
end
Squares.squared_5
#â 25
Quoted Fragments quote
, (bind_quoted:
). .
defmacro __using__(opts) do
keys = Keyword.keys(opts[:fields])
quote location: :keep, bind_quoted: [keys: keys] do
type =
{:%{}, [],
[
{:__struct__, {:__MODULE__, [], ruby}},
{:version, {:atom, [], []}}
| Enum.zip(keys, Stream.cycle([{:atom, [], []}]))
]}
# âââââââââââââ
@type t :: unquote(type)
defstruct keys
end
end
unquote/1
, bind_quoted:
quote/2
.
!