OvermindDL1/protocol_ex
{ "createdAt": "2017-07-12T17:43:17Z", "defaultBranch": "master", "description": "Elixir Extended Protocol", "fullName": "OvermindDL1/protocol_ex", "homepage": "https://hex.pm/packages/protocol_ex", "language": "Elixir", "name": "protocol_ex", "pushedAt": "2024-05-23T20:44:20Z", "stargazersCount": 49, "topics": [], "updatedAt": "2024-10-17T05:36:32Z", "url": "https://github.com/OvermindDL1/protocol_ex"}ProtocolEx
Section titled “ProtocolEx”Extended Protocol library.
Performs matching for protocol implementations instead of being limited to certain base types as in standard Elixir Protocols.
Installation
Section titled “Installation”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], # ... ]endThe below assumes:
import ProtocolExdefprotocol_ex/2
Section titled “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)endPlain 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.
Example
Section titled “Example”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)enddeftest example
Section titled “deftest example”In 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) endendYou can cancel running tests on compile by passing --no-protocol-tests to the mix compile command.
Named position example
Section titled “Named position example”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)endIn this example wrap/2 uses the monad matcher in its last position, where flat_map/2 uses it in the first.
defimpl_ex/4
Section titled “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.
Example
Section titled “Example”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: 42end
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)}endresolve_protocol_ex/2
Section titled “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.
Example
Section titled “Example”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.
Protocol Usage
Section titled “Protocol Usage”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()Debugging
Section titled “Debugging”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.