Implement protocol support for structs

This commit is contained in:
José Valim
2014-02-06 15:56:21 +01:00
parent 4917bfca29
commit 1f0861771a
3 changed files with 184 additions and 157 deletions
+74 -80
View File
@@ -33,13 +33,36 @@ defmodule Protocol do
defp after_defprotocol do
quote unquote: false do
{ arg, bodies, rec } = Protocol.impl_for(__MODULE__)
# Get all builtin types and add Any to have all modules.
builtin = Protocol.builtin
all = [Any] ++ lc { guard, mod } inlist builtin, do: mod
# == Deprecated records handling ==
{ arg, impl } = Protocol.rec_impl_for(__MODULE__, all)
Kernel.def impl_for(unquote(arg)) when Kernel.is_record(unquote(arg)), do: unquote(impl)
# == Deprecated records handling ==
@spec impl_for(term) :: module | nil
Kernel.def impl_for(data)
lc { guard, body } inlist bodies do
Kernel.def impl_for(unquote(arg)) when unquote(guard), do: unquote(body)
# Define the implementation for structs.
#
# It simply delegates to struct_impl_for which is then
# optimized during protocol consolidation.
Kernel.def impl_for(%{ __struct__: struct }) when
:erlang.is_atom(struct) and not(struct in unquote(all)) do
struct_impl_for(struct)
end
# Define the implementation for builtins.
lc { guard, mod } inlist builtin do
target = Module.concat(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
unquote(target).__impl__(:name)
catch :error, :undef, [[{ unquote(target), :__impl__, [:name], _ }|_]|_] ->
any_impl_for
end
end
@spec impl_for!(term) :: module | no_return
@@ -47,10 +70,7 @@ defmodule Protocol do
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
end
# Handle special Record type
Kernel.defp rec_impl_for(unquote(arg)), do: unquote(rec)
# Handle special Any type
# Internal handler for Any
if @fallback_to_any do
Kernel.defp any_impl_for do
try do
@@ -64,8 +84,18 @@ defmodule Protocol do
Kernel.defp any_impl_for, do: nil
end
# Inline both helpers
@compile { :inline, any_impl_for: 0, rec_impl_for: 1 }
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
target = Module.concat(__MODULE__, struct)
try do
target.__impl__(:name)
catch :error, :undef, [[{ ^target, :__impl__, [:name], _ }|_]|_] ->
any_impl_for
end
end
# Inline any implementation for.
@compile { :inline, any_impl_for: 0, struct_impl_for: 1 }
if :code.ensure_loaded(Kernel.Typespec) == { :module, Kernel.Typespec } and
not Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
@@ -139,88 +169,52 @@ defmodule Protocol do
# Builtin types.
@doc false
def builtin do
[ Tuple, Atom, List, Map, BitString, Integer, Float,
Function, PID, Port, Reference, Any ]
[ is_tuple: Tuple,
is_atom: Atom,
is_list: List,
is_map: Map,
is_bitstring: BitString,
is_integer: Integer,
is_float: Float,
is_function: Function,
is_pid: PID,
is_port: Port,
is_reference: Reference ]
end
# Implements the function that detects the protocol and
# returns the module to dispatch to.
@doc false
def impl_for(current) do
arg = quote(do: arg)
all = [Record|builtin]
def rec_impl_for(current, all) do
arg = quote do: arg
target = Module.concat(current, Tuple)
{ arg,
lc(mod inlist all, do: impl_for(current, mod, arg)),
rec_impl_for(current, arg) }
end
defp rec_impl_for(current, arg) do
fallback = impl_for(current, Tuple, arg) |> elem(1)
quote do
target = Module.concat(unquote(current), unquote(arg))
fallback = quote do
try do
target.__impl__(:name)
catch
:error, :undef, [[{ ^target, :__impl__, [:name], _ }|_]|_] ->
unquote(target).__impl__(:name)
catch :error, :undef, [[{ unquote(target), :__impl__, [:name], _ }|_]|_] ->
any_impl_for
end
end
impl_for = quote do
atom = :erlang.element(1, unquote(arg))
case not(atom in unquote(all)) and match?('Elixir.' ++ _, atom_to_list(atom)) do
true ->
target = Module.concat(unquote(current), atom)
try do
target.__impl__(:name)
catch
:error, :undef, [[{ ^target, :__impl__, [:name], _ }|_]|_] ->
unquote(fallback)
end
false ->
unquote(fallback)
end
end
end
defp impl_for(current, Record, arg) do
fallback = impl_for(current, Tuple, arg) |> elem(1)
dispatch = quote do
atom = :erlang.element(1, unquote(arg))
case not(atom in unquote(builtin)) and match?('Elixir.' ++ _, atom_to_list(atom)) do
true -> rec_impl_for(atom)
false -> unquote(fallback)
end
end
quote do
{ is_record(unquote(arg)), unquote(dispatch) }
end
end
defp impl_for(current, Tuple, arg), do: impl_with_fallback(Tuple, :is_tuple, current, arg)
defp impl_for(current, Atom, arg), do: impl_with_fallback(Atom, :is_atom, current, arg)
defp impl_for(current, List, arg), do: impl_with_fallback(List, :is_list, current, arg)
defp impl_for(current, Map, arg), do: impl_with_fallback(Map, :is_map, current, arg)
defp impl_for(current, BitString, arg), do: impl_with_fallback(BitString, :is_bitstring, current, arg)
defp impl_for(current, Integer, arg), do: impl_with_fallback(Integer, :is_integer, current, arg)
defp impl_for(current, Float, arg), do: impl_with_fallback(Float, :is_float, current, arg)
defp impl_for(current, Function, arg), do: impl_with_fallback(Function, :is_function, current, arg)
defp impl_for(current, PID, arg), do: impl_with_fallback(PID, :is_pid, current, arg)
defp impl_for(current, Port, arg), do: impl_with_fallback(Port, :is_port, current, arg)
defp impl_for(current, Reference, arg), do: impl_with_fallback(Reference, :is_reference, current, arg)
defp impl_for(_current, Any, _arg) do
{ true, quote(do: any_impl_for) }
end
# Defines an implementation with fallback to the given module.
defp impl_with_fallback(mod, guard, current, arg) do
quote do
{ :erlang.unquote(guard)(unquote(arg)),
unquote(with_fallback(Module.concat(current, mod))) }
end
end
# Tries to dispatch to a given target, fallbacks to the
# given `fallback` implementation if the target does not exist.
defp with_fallback(target) when is_atom(target) do
quote do
try do
unquote(target).__impl__(:name)
catch
:error, :undef, [[{ unquote(target), :__impl__, [:name], _ }|_]|_] ->
any_impl_for
end
end
{ arg, impl_for }
end
end
+37 -58
View File
@@ -160,36 +160,38 @@ defmodule Protocol.Consolidation do
# impl_for/1 dispatch version.
defp change_debug_info({ protocol, any, code }, types) do
types = if any, do: types, else: List.delete(types, Any)
records = types -- Protocol.builtin
builtin = Protocol.builtin -- (Protocol.builtin -- types)
builtin = if records != [], do: [Record|builtin], else: builtin
change_impl_for(code, protocol, builtin, records, false, [])
all = [Any] ++ lc { _guard, mod } inlist Protocol.builtin, do: mod
structs = types -- all
change_impl_for(code, protocol, types, structs, false, [])
end
defp change_impl_for([{ :attribute, line, :protocol, _ }|t], protocol, builtin, records, _, acc) do
attr = [fallback_to_any: Any in builtin, consolidated: true]
change_impl_for(t, protocol, builtin, records, true,
[{ :attribute, line, :protocol, attr }|acc])
defp change_impl_for([{ :attribute, line, :protocol, opts }|t], protocol, types, structs, _, acc) do
opts = [fallback_to_any: opts[:fallback_to_any], consolidated: true]
change_impl_for(t, protocol, types, structs, true,
[{ :attribute, line, :protocol, opts }|acc])
end
defp change_impl_for([{ :function, line, :impl_for, 1, _ }|t], protocol, builtin, records, is_protocol, acc) do
clauses = lc type inlist builtin, do: clause_for(type, protocol, line)
defp change_impl_for([{ :function, line, :impl_for, 1, _ }|t], protocol, types, structs, is_protocol, acc) do
fallback = if Any in types, do: Module.concat(protocol, Any), else: nil
unless Any in builtin do
clauses = clauses ++ [fallback_clause_for(nil, protocol, line)]
end
clauses = lc { guard, mod } inlist Protocol.builtin,
mod in types,
do: builtin_clause_for(mod, guard, protocol, line)
change_impl_for(t, protocol, builtin, records, is_protocol,
clauses = [struct_clause_for(line)|clauses] ++
[fallback_clause_for(fallback, protocol, line)]
change_impl_for(t, protocol, types, structs, is_protocol,
[{ :function, line, :impl_for, 1, clauses }|acc])
end
defp change_impl_for([{ :function, line, :rec_impl_for, 1, _ }|t], protocol, builtin, records, is_protocol, acc) do
fallback = if Tuple in builtin, do: Module.concat(protocol, Tuple)
clauses = lc type inlist records, do: record_clause_for(type, protocol, line)
clauses = clauses ++ [fallback_clause_for(fallback, protocol, line)]
defp change_impl_for([{ :function, line, :struct_impl_for, 1, _ }|t], protocol, types, structs, is_protocol, acc) do
fallback = if Any in types, do: Module.concat(protocol, Any), else: nil
clauses = lc struct inlist structs, do: each_struct_clause_for(struct, protocol, line)
clauses = clauses ++ [fallback_clause_for(fallback, protocol, line)]
change_impl_for(t, protocol, builtin, records, is_protocol,
[{ :function, line, :rec_impl_for, 1, clauses }|acc])
change_impl_for(t, protocol, types, structs, is_protocol,
[{ :function, line, :struct_impl_for, 1, clauses }|acc])
end
defp change_impl_for([h|t], protocol, info, types, is_protocol, acc) do
@@ -204,43 +206,6 @@ defmodule Protocol.Consolidation do
end
end
defp clause_for(Tuple, protocol, line), do: builtin_clause_for(Tuple, :is_tuple, protocol, line)
defp clause_for(Atom, protocol, line), do: builtin_clause_for(Atom, :is_atom, protocol, line)
defp clause_for(List, protocol, line), do: builtin_clause_for(List, :is_list, protocol, line)
defp clause_for(Map, protocol, line), do: builtin_clause_for(Map, :is_map, protocol, line)
defp clause_for(BitString, protocol, line), do: builtin_clause_for(BitString, :is_bitstring, protocol, line)
defp clause_for(Integer, protocol, line), do: builtin_clause_for(Integer, :is_integer, protocol, line)
defp clause_for(Float, protocol, line), do: builtin_clause_for(Float, :is_float, protocol, line)
defp clause_for(Function, protocol, line), do: builtin_clause_for(Function, :is_function, protocol, line)
defp clause_for(PID, protocol, line), do: builtin_clause_for(PID, :is_pid, protocol, line)
defp clause_for(Port, protocol, line), do: builtin_clause_for(Port, :is_port, protocol, line)
defp clause_for(Reference, protocol, line), do: builtin_clause_for(Reference, :is_reference, protocol, line)
defp clause_for(Any, protocol, line) do
{:clause, line, [{:var, line, :_}], [],
[{ :atom, line, Module.concat(protocol, Any) }]}
end
defp clause_for(Record, _protocol, line) do
{:clause, line, [{:var, line, :x}],
[[{:op, line, :andalso,
{:call, line,
{:remote, line, {:atom, line, :erlang}, {:atom, line, :is_tuple}},
[{:var, line, :x}]},
{:call, line,
{:remote, line, {:atom, line, :erlang}, {:atom, line, :is_atom}},
[{:call, line,
{:remote, line, {:atom, line, :erlang}, {:atom, line, :element}},
[{:integer, line, 1}, {:var, line, :x}]
}]},
}]],
[{:call, line,
{:atom, line, :rec_impl_for},
[{:call, line,
{:remote, line, {:atom, line, :erlang}, {:atom, line, :element}},
[{:integer, line, 1}, {:var, line, :x}]}]}]}
end
defp builtin_clause_for(mod, guard, protocol, line) do
{:clause, line,
[{:var, line, :x}],
@@ -251,7 +216,21 @@ defmodule Protocol.Consolidation do
[{:atom, line, Module.concat(protocol, mod)}]}
end
defp record_clause_for(other, protocol, line) do
defp struct_clause_for(line) do
{:clause, line,
[{:map, line, [
{:map_field_exact, line, {:atom, line, :__struct__}, {:var, line, :x}}
]}],
[[{:call, line,
{:remote, line, {:atom, line, :erlang}, {:atom, line, :is_atom}},
[{:var, line, :x}],
}]],
[{:call, line,
{:atom, line, :struct_impl_for},
[{:var, line, :x}]}]}
end
defp each_struct_clause_for(other, protocol, line) do
{:clause, line, [{:atom, line, other}], [],
[{:atom, line, Module.concat(protocol, other)}]}
end
+73 -19
View File
@@ -38,12 +38,42 @@ defmodule ProtocolTest do
end
end
defimpl WithAny, for: Map do
def ok(_map) do
:ok
end
end
defimpl WithAny, for: Any do
def ok(_any) do
:ok
end
end
defmodule NoImplStruct do
def __struct__ do
%{ a: 0, b: 0 }
end
end
defmodule ImplStruct do
def __struct__ do
%{ a: 0, b: 0 }
end
defimpl Sample do
def ok(struct) do
Unknown.undefined(struct)
end
end
end
defimpl WithAny, for: ImplStruct do
def ok(_struct) do
:ok
end
end
test "protocol implementations without any" do
assert nil? Sample.impl_for(:foo)
assert nil? Sample.impl_for(fn(x) -> x end)
@@ -64,6 +94,11 @@ defmodule ProtocolTest do
Sample.ProtocolTest.ImplRec
assert Sample.impl_for(NoImplRec[]) ==
nil
assert Sample.impl_for(%ImplStruct{}) ==
Sample.ProtocolTest.ImplStruct
assert Sample.impl_for(%NoImplStruct{}) ==
nil
end
test "protocol implementation with any and records fallback" do
@@ -74,6 +109,15 @@ defmodule ProtocolTest do
assert WithAny.impl_for(self) == WithAny.Any
end
test "protocol implementation with any and structs fallback" do
assert WithAny.impl_for(%ImplStruct{}) == WithAny.ProtocolTest.ImplStruct
assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any
assert WithAny.impl_for(%{ __struct__: "foo" }) == WithAny.Map
assert WithAny.impl_for(%{ __struct__: Tuple }) == WithAny.Map
assert WithAny.impl_for(%{}) == WithAny.Map
assert WithAny.impl_for(self) == WithAny.Any
end
test "protocol not implemented" do
assert_raise Protocol.UndefinedError, "protocol ProtocolTest.Sample not implemented for :foo", fn ->
Sample.ok(:foo)
@@ -88,7 +132,7 @@ defmodule ProtocolTest do
test "protocol keeps underlying UndefinedFunctionError" do
assert_raise UndefinedFunctionError, fn ->
WithAll.ok(Foo.new)
WithAll.ok(%ImplStruct{})
end
end
@@ -110,17 +154,17 @@ defmodule ProtocolTest do
def test(thing)
end
defimpl Attribute, for: ImplRec do
defimpl Attribute, for: ImplStruct do
def test(_) do
{ @protocol, @for }
end
end
assert Attribute.test(ImplRec[]) == { Attribute, ImplRec }
assert Attribute.ProtocolTest.ImplRec.__impl__(:protocol) == Attribute
assert Attribute.ProtocolTest.ImplRec.__impl__(:for) == ImplRec
assert Attribute.ProtocolTest.ImplRec.__info__(:attributes)[:impl] ==
[protocol: Attribute, for: ImplRec]
assert Attribute.test(%ImplStruct{}) == { Attribute, ImplStruct }
assert Attribute.ProtocolTest.ImplStruct.__impl__(:protocol) == Attribute
assert Attribute.ProtocolTest.ImplStruct.__impl__(:for) == ImplStruct
assert Attribute.ProtocolTest.ImplStruct.__info__(:attributes)[:impl] ==
[protocol: Attribute, for: ImplStruct]
end
test "defimpl with multiple for" do
@@ -174,17 +218,26 @@ end
# end
# )
#
# defrecord ImplRec, a: 0, b: 0
# defrecord NoImplRec, a: 0
# defmodule NoImplStruct do
# def __struct__ do
# %{ a: 0, b: 0 }
# end
# end
#
# defmodule ImplStruct do
# def __struct__ do
# %{ a: 0, b: 0 }
# end
# end
#
# Code.append_path(path)
#
# # Any is ignored because there is no fallback
# { :ok, binary } = Protocol.Consolidation.apply_to(Sample, [Any, ImplRec, NoImplRec])
# { :ok, binary } = Protocol.Consolidation.apply_to(Sample, [Any, ImplStruct])
# :code.load_binary(Sample, 'protocol_test.exs', binary)
#
# # Any should be moved to the end
# { :ok, binary } = Protocol.Consolidation.apply_to(WithAny, [Any, ImplRec, Tuple])
# { :ok, binary } = Protocol.Consolidation.apply_to(WithAny, [Any, ImplStruct, Map])
# :code.load_binary(WithAny, 'protocol_test.exs', binary)
#
# test "consolidated implementations without any" do
@@ -204,18 +257,19 @@ end
# assert nil? Sample.impl_for(make_ref)
# assert nil? Sample.impl_for(Macro.Env[])
#
# assert Sample.impl_for(ImplRec[]) ==
# Sample.Protocol.ConsolidationTest.ImplRec
# assert Sample.impl_for(NoImplRec[]) ==
# assert Sample.impl_for(%ImplStruct{}) ==
# Sample.Protocol.ConsolidationTest.ImplStruct
# assert Sample.impl_for(%NoImplStruct{}) ==
# nil
# end
#
# test "consolidated implementations with any and tuple fallback" do
# assert WithAny.impl_for(ImplRec[]) == WithAny.Protocol.ConsolidationTest.ImplRec
# assert WithAny.impl_for(NoImplRec[]) == WithAny.Tuple
# assert WithAny.impl_for({ :foo }) == WithAny.Tuple
# assert WithAny.impl_for({}) == WithAny.Tuple
# assert WithAny.impl_for(self) == WithAny.Any
# assert WithAny.impl_for(%ImplStruct{}) == WithAny.Protocol.ConsolidationTest.ImplStruct
# assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any
# assert WithAny.impl_for(%{ __struct__: "foo" }) == WithAny.Map
# assert WithAny.impl_for(%{ __struct__: Tuple }) == WithAny.Map
# assert WithAny.impl_for(%{}) == WithAny.Map
# assert WithAny.impl_for(self) == WithAny.Any
# end
#
# test "consolidation keeps docs" do