Skip to content

Commit 2b3e536

Browse files
author
José Valim
committed
Provide Protocol.derive/3
1 parent dff93a4 commit 2b3e536

File tree

6 files changed

+75
-24
lines changed

6 files changed

+75
-24
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3094,7 +3094,7 @@ defmodule Kernel do
30943094
30953095
For each protocol given to `@derive`, Elixir will assert there is an
30963096
implementation of that protocol for maps and check if the map
3097-
implementation defines a `__deriving__/2` callback. If so, the callback
3097+
implementation defines a `__deriving__/3` callback. If so, the callback
30983098
is invoked, otherwise an implementation that simply points to the map
30993099
one is automatically derived.
31003100
@@ -3125,18 +3125,18 @@ defmodule Kernel do
31253125
other -> raise ArgumentError, "struct field names must be atoms, got: #{inspect other}"
31263126
end, fields)
31273127

3128+
@struct :maps.put(:__struct__, __MODULE__, :maps.from_list(fields))
3129+
31283130
case Module.get_attribute(__MODULE__, :derive) do
3129-
nil ->
3131+
[] ->
31303132
:ok
31313133
derive ->
3132-
env = __ENV__
3133-
struct = :maps.put(:__struct__, __MODULE__, :maps.from_list(fields))
3134-
:lists.foreach(fn proto -> Protocol.__derive__(proto, struct, env) end, derive)
3134+
Protocol.__derive__(derive, __MODULE__, __ENV__)
31353135
end
31363136

31373137
@spec __struct__() :: t
31383138
def __struct__() do
3139-
%{unquote_splicing(Macro.escape(fields)), __struct__: __MODULE__}
3139+
@struct
31403140
end
31413141
end
31423142

lib/elixir/lib/protocol.ex

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,16 @@ defmodule Protocol do
111111
end
112112
end
113113

114+
@doc """
115+
Derive the `protocol` for `module` with the given options.
116+
"""
117+
defmacro derive(protocol, module, options \\ []) do
118+
quote do
119+
module = unquote(module)
120+
Protocol.__derive__([{unquote(protocol), unquote(options)}], module, __ENV__)
121+
end
122+
end
123+
114124
## Consolidation
115125

116126
@doc """
@@ -527,17 +537,34 @@ defmodule Protocol do
527537
end
528538

529539
@doc false
530-
def __derive__(protocol, struct, %Macro.Env{} = env) do
531-
for = env.module
532-
impl = Module.concat(protocol, Map)
540+
def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
541+
struct =
542+
if for == env.module do
543+
Module.get_attribute(for, :struct) ||
544+
raise "struct is not defined for #{inspect for}"
545+
else
546+
for.__struct__
547+
end
548+
549+
:lists.foreach(fn
550+
proto when is_atom(proto) ->
551+
derive(proto, for, struct, [], env)
552+
{proto, opts} when is_atom(proto) ->
553+
derive(proto, for, struct, opts, env)
554+
end, :lists.flatten(derives))
533555

534-
extra = ", cannot derive #{inspect protocol} for #{inspect env.module}"
556+
:ok
557+
end
558+
559+
defp derive(protocol, for, struct, opts, env) do
560+
impl = Module.concat(protocol, Map)
561+
extra = ", cannot derive #{inspect protocol} for #{inspect for}"
535562
assert_protocol!(protocol, extra)
536563
assert_impl!(protocol, impl, extra)
537564

538565
# Clean up variables from eval context
539-
env = %{env | vars: []}
540-
args = [env, struct]
566+
env = %{env | vars: [], export_vars: nil}
567+
args = [for, struct, opts]
541568

542569
:elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn
543570
mod, fun, args ->
@@ -556,8 +583,6 @@ defmodule Protocol do
556583
end)
557584
end
558585
end)
559-
560-
:ok
561586
end
562587

563588
@doc false

lib/elixir/src/elixir_exp.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ expand({'try', Meta, [KV]}, E) ->
243243

244244
%% Comprehensions
245245

246-
expand({for, Meta, Args}, E) when is_list(Args) ->
246+
expand({for, Meta, [_|_] = Args}, E) ->
247247
elixir_for:expand(Meta, Args, E);
248248

249249
%% Super

lib/elixir/src/elixir_module.erl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ build(Line, File, Module, Lexical) ->
112112
end,
113113

114114
Attributes = [behaviour, on_load, spec, type, export_type, opaque, callback, compile],
115-
ets:insert(DataTable, {?acc_attr, [before_compile, after_compile, on_definition|Attributes]}),
115+
ets:insert(DataTable, {?acc_attr, [before_compile, after_compile, on_definition, derive|Attributes]}),
116116
ets:insert(DataTable, {?persisted_attr, [vsn|Attributes]}),
117117
ets:insert(DataTable, {?docs_attr, ets:new(DataTable, [ordered_set, public])}),
118118
ets:insert(DataTable, {?lexical_attr, Lexical}),
@@ -128,7 +128,7 @@ build(Line, File, Module, Lexical) ->
128128
eval_form(Line, Module, Block, Vars, E) ->
129129
{Value, EE} = elixir_compiler:eval_forms(Block, Vars, E),
130130
elixir_def_overridable:store_pending(Module),
131-
EV = elixir_env:linify({Line, EE#{vars := []}}),
131+
EV = elixir_env:linify({Line, EE#{vars := [], export_vars := nil}}),
132132
EC = eval_callbacks(Line, Module, before_compile, [EV], EV),
133133
elixir_def_overridable:store_pending(Module),
134134
{Value, EC}.
@@ -137,7 +137,8 @@ eval_callbacks(Line, Module, Name, Args, E) ->
137137
Callbacks = lists:reverse(ets:lookup_element(data_table(Module), Name, 2)),
138138

139139
lists:foldl(fun({M,F}, Acc) ->
140-
expand_callback(Line, M, F, Args, Acc#{vars := []}, fun(AM, AF, AA) -> apply(AM, AF, AA) end)
140+
expand_callback(Line, M, F, Args, Acc#{vars := [], export_vars := nil},
141+
fun(AM, AF, AA) -> apply(AM, AF, AA) end)
141142
end, E, Callbacks).
142143

143144
%% Return the form with exports and function declarations.

lib/elixir/src/elixir_translator.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ translate({'receive', Meta, [KV]}, #elixir_scope{return=Return} = RS) when is_li
129129

130130
%% Comprehensions
131131

132-
translate({for, Meta, Args}, S) when is_list(Args) ->
132+
translate({for, Meta, [_|_] = Args}, S) ->
133133
elixir_for:translate(Meta, Args, S);
134134

135135
%% Super

lib/elixir/test/elixir/protocol_test.exs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ defmodule ProtocolTest do
2121
end
2222

2323
defimpl Derivable, for: Map do
24-
defmacro __deriving__(_env, struct) do
24+
defmacro __deriving__(module, struct, options) do
2525
quote do
26-
defimpl Derivable do
26+
defimpl Derivable, for: unquote(module) do
2727
def ok(arg) do
28-
{:ok, arg, unquote(Macro.escape(struct))}
28+
{:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
2929
end
3030
end
3131
end
@@ -176,17 +176,42 @@ defmodule ProtocolTest do
176176

177177
test "custom derive implementation" do
178178
struct = %ImplStruct{a: 1, b: 1}
179-
assert Derivable.ok(struct) == {:ok, struct, %ImplStruct{}}
179+
assert Derivable.ok(struct) == {:ok, struct, %ImplStruct{}, []}
180180

181181
struct = %ImplStruct{a: 1, b: 1}
182-
assert Derivable.ok(struct) == {:ok, struct, %ImplStruct{}}
182+
assert Derivable.ok(struct) == {:ok, struct, %ImplStruct{}, []}
183183

184184
assert_raise Protocol.UndefinedError, fn ->
185185
struct = %NoImplStruct{a: 1, b: 1}
186186
Derivable.ok(struct)
187187
end
188188
end
189189

190+
test "custom derive implementation with options" do
191+
defmodule AnotherStruct do
192+
@derive [{Derivable, :ok}]
193+
@derive [WithAny]
194+
defstruct a: 0, b: 0
195+
end
196+
197+
struct = struct AnotherStruct, a: 1, b: 1
198+
assert Derivable.ok(struct) ==
199+
{:ok, struct, struct(AnotherStruct), :ok}
200+
end
201+
202+
test "custom derive implementation via API" do
203+
defmodule InlineStruct do
204+
defstruct a: 0, b: 0
205+
end
206+
207+
require Protocol
208+
assert Protocol.derive(Derivable, InlineStruct, :oops) == :ok
209+
210+
struct = struct InlineStruct, a: 1, b: 1
211+
assert Derivable.ok(struct) ==
212+
{:ok, struct, struct(InlineStruct), :oops}
213+
end
214+
190215
test "cannot derive without a map implementation" do
191216
assert_raise ArgumentError,
192217
~r"#{inspect Sample.Map} is not available, cannot derive #{inspect Sample}", fn ->

0 commit comments

Comments
 (0)