Extended Protocol library.
Performs matching for protocol implementations instead of being limited to certain base types as in standard Elixir Protocols.
Available in Hex with Documentation, the package can be installed
by adding :protocol_ex
to your list of dependencies in mix.exs
:
{:protocol_ex, "~> 0.3.0"},
For auto-consolidation add the compiler to your mix.exs
definition like (make certain it comes after the built-in elixir compiler):
def project do
[
# ...
compilers: Mix.compilers ++ [:protocol_ex],
# ...
]
end
The below assumes:
import ProtocolEx
defprotocol_ex/2
defprotocol_ex/2
is used like defmodule
in that it takes a module name to become and the body. The body can contain plain function heads like:
def something(a)
def blah(a, b)
Or it can contain full bodies:
def bloop(a) do
to_string(a)
end
Plain heads must be implemented in an implementation, not doing so will raise an error.
Full body functions supply the fallback, if an implementation does not supply an implementation of it then it will fall back to the fallback implementation.
Inside a defprotocol_ex/2
you are able to use deftest
to run some tests at compile time to make certain that the implementations follow necessary rules.
defprotocol_ex Blah do
def empty() # Transformed to 1-arg that matches on based on the implementation, but ignored otherwise
def succ(a)
def add(a, b)
def map(a, f) when is_function(f, 1)
def a_fallback(a), do: inspect(a)
end
deftest
exampleIn this example each implementation must also define a prop_generator
that returns a StreamData generator to generate the types of that implementation, such as for lists: def prop_generator(), do: StreamData.list_of(StreamData.integer())
defprotocol_ex Functor do
def map(v, f)
deftest identity do
StreamData.check_all(prop_generator(), [initial_seed: :os.timestamp()], fn v ->
if v === map(v, &(&1)) do
{:ok, v}
else
{:error, v}
end
end)
end
deftest composition do
f = fn x -> x end
g = fn x -> x end
StreamData.check_all(prop_generator(), [initial_seed: :os.timestamp()], fn v ->
if map(v, fn x -> f.(g.(x)) end) === map(map(v, g), f) do
{:ok, v}
else
{:error, v}
end
end)
end
end
You can cancel running tests on compile by passing --no-protocol-tests
to the mix compile
command.
You can also specify a name to the matcher so you can use the same name in a specific position in a def
, like in:
defprotocol_ex Monad, as: monad do
def wrap(value, monad)
def flat_map(monad, fun)
end
In this example wrap/2
uses the monad matcher in its last position, where flat_map/2
uses it in the first.
defimpl_ex/4
defimpl_ex/4
takes a unique name for this implementation for the given protocol first, then a normal elixir match expression second, then [for: ProtocolName]
for a given protocol, and lastly the body.
defimpl_ex Integer, i when is_integer(i), for: Blah do
def empty(), do: 0
defmacro succ(i), do: quote(do: unquote(i)+1) # Macro's get inlined into the protocol itself
def add(i, b), do: i+b
def map(i, f), do: f.(i)
def a_fallback(i), do: "Integer: #{i}"
end
defimpl_ex TaggedTuple.Vwoop, {Vwoop, i} when is_integer(i), for: Blah do
def empty(), do: {Vwoop, 0}
def succ({Vwoop, i}), do: {Vwoop, i+1}
def add({Vwoop, i}, b), do: {Vwoop, i+b}
def map({Vwoop, i}, f), do: {Vwoop, f.(i)}
end
defmodule MyStruct do
defstruct a: 42
end
defimpl_ex MineOlStruct, %MyStruct{}, for: Blah do
def empty(), do: %MyStruct{a: 0}
def succ(s), do: %{s | a: s.a+1}
def add(s, b), do: %{s | a: s.a+b}
def map(s, f), do: %{s | a: f.(s.a)}
end
resolve_protocol_ex/2
resolve_protocol_ex/2
allows to dynamic consolidation (or if you do not wish to use the compiler). It takes the protocol module name first, then a list of the unique names to consolidate. If there is more than one implementation that can match a given value then they are used in the order of definition here.
ProtocolEx.resolve_protocol_ex(Blah, [
Integer,
TaggedTuple.Vwoop,
MineOlStruct,
])
This can be called again at runtime if so wished, it allows you rebuild the protocol consolidation module to remove or add implementations such as for dynamic plugins.
To use your protocol you just call the specific functions on the module, for the above examples then all of these will work:
0 = Blah.empty(42)
{Vwoop, 0} = Blah.empty({Vwoop, 42})
%MyStruct{a: 0} = Blah.empty(%MyStruct{a: 42})
43 = Blah.succ(42)
{Vwoop, 43} = Blah.succ({Vwoop, 42})
%MyStruct{a: 43} = Blah.succ(%MyStruct{a: 42})
47 = Blah.add(42, 5)
{Vwoop, 47} = Blah.add({Vwoop, 42}, 5)
%MyStruct{a: 47} = Blah.add(%MyStruct{a: 42}, 5)
"Integer: 42" = Blah.a_fallback(42)
"{Vwoop, 42}" = Blah.a_fallback({Vwoop, 42})
"%MyStruct{a: 42}" = Blah.a_fallback(%MyStruct{a: 42})
43 = Blah.map(42, &(&1+1))
{Vwoop, 43} = Blah.map({Vwoop, 42}, &(&1+1))
%MyStruct{a: 43} = Blah.map(%MyStruct{a: 42}, &(&1+1))
It can of course be useful to call an implementation directly as well:
0 = Blah.Integer.empty()
{Vwoop, 0} = Blah.TaggedTuple.Vwoop.empty()
%MyStruct{a: 0} = Blah.MineOlStruct.empty()
As with the standard Elixir Protocols, running mix compile
with the --verbose
flag like mix compile --verbose
will state what protocol_ex's it found and with what implementations it found to combine it with.
You can also use the --print-protocol-ex
flag to print out the resultant compiled protocol source itself. Do note that this source file itself may not be compileable and/or is very unlikely to work as Elixirs AST is not homioiconic and thus it loses specific contextual information.