Hex: protocol_ex | Hex
Docs: ProtocolEx — protocol_ex v0.4.4
Source: GitHub - OvermindDL1/protocol_ex: Elixir Extended Protocol
Made yet another library, this time it is an enhanced protocol style thing that is not limited to base types and structs as the built-in one is, and I’ve not got around to making a compiler pass like the built-in one has yet so right now you just need to consolidate it manually anywhere (which is just a single call), so first, simple example usage in iex:
iex> import ProtocolEx
ProtocolEx
iex> defprotocolEx Blah do
...> def empty(a)
...> def succ(a)
...> def add(a, b)
...>
...> def a_fallback(a), do: inspect(a)
...> end
{:module, :"Elixir.Blah.$ProtocolEx_description$",
<<70, 79, 82, 49, 0, 0, 6, 72, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 127,
131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:spec, 0}}
iex> defimplEx Integer, i when is_integer(i), for: Blah do
...> def empty(_), do: 0
...> def succ(i), do: i+1
...> def add(i, b), do: i+b
...>
...> def a_fallback(i), do: "Integer: #{i}"
...> end
:ok
iex> # An error when you do not fullfill the protocol properly:
nil
iex> defimplEx TaggedTuple.Vwoop, {Vwoop, i} when is_integer(i), for: Blah do
...> def empty(_), do: {Vwoop, 0}
...> end
** (ProtocolEx.MissingRequiredProtocolDefinition) On Protocol `Elixir.Blah` missing a required protocol callback on `TaggedTuple.Vwoop` of: add/2
(protocol_ex) lib/protocol_ex.ex:194: anonymous fn/3 in ProtocolEx.verify_valid_spec_on_module/3
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
iex>
nil
iex> defimplEx 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}
...> end
:ok
iex> defmodule MyStruct do
...> defstruct a: 42
...> end
{:module, MyStruct,
<<70, 79, 82, 49, 0, 0, 9, 12, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 186,
131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, %MyStruct{a: 42}}
iex> defimplEx 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}
...> end
:ok
iex> # Now let's consolidate it manually (you can even call this at runtime to dynamically add/remove protocol implementations too
nil
iex> ProtocolEx.resolveProtocolEx(Blah, [
...> Integer,
...> TaggedTuple.Vwoop,
...> MineOlStruct,
...> ])
:ok
iex> # and let's call it with some tests:
nil
iex> 0 = Blah.empty(42)
0
iex> {Vwoop, 0} = Blah.empty({Vwoop, 42})
{Vwoop, 0}
iex> %MyStruct{a: 0} = Blah.empty(%MyStruct{a: 42})
%MyStruct{a: 0}
iex>
nil
iex> 43 = Blah.succ(42)
43
iex> {Vwoop, 43} = Blah.succ({Vwoop, 42})
{Vwoop, 43}
iex> %MyStruct{a: 43} = Blah.succ(%MyStruct{a: 42})
%MyStruct{a: 43}
iex>
nil
iex> 47 = Blah.add(42, 5)
47
iex> {Vwoop, 47} = Blah.add({Vwoop, 42}, 5)
{Vwoop, 47}
iex> %MyStruct{a: 47} = Blah.add(%MyStruct{a: 42}, 5)
%MyStruct{a: 47}
iex>
nil
iex> "Integer: 42" = Blah.a_fallback(42)
"Integer: 42"
iex> "{Vwoop, 42}" = Blah.a_fallback({Vwoop, 42})
"{Vwoop, 42}"
iex> "%MyStruct{a: 42}" = Blah.a_fallback(%MyStruct{a: 42})
"%MyStruct{a: 42}"
Should work properly when aliased out and in sub-modules and all forth as well (as the tests test for).
But basically a defprotocolEx
is simple, no guards allowed (might change on non-first arguments actually, I’m tempted…), either empty function heads, which must be implemented, or a full function with body, which becomes the default implementation if not implemented (a fallback, also great to add functions to call the protocol functions too).
In the defimplEx
it takes a unique ‘name’ for this implementation (so it can be referred to later), a match specification, a for: ProtocolName
to specify the protocol, and the module body. (Implementation detail: For a given implementation named Bloop
for a protocol named Blah
creates a module of Bloop.Blah
that you can call directly).
The resolveProtocolEx
takes the protocol to consolidate, and a list of the implementations to make into the final module, they are tested based on their matchspec in the order specified here, so for the above iex session it generates this for the protocol module:
def Blah do
# Snip some extra stuff like the __protocolEx__ description function
def(empty(i = a) when is_integer(i)) do
Testering.Blah.Integer.empty(a)
end
def(empty({Vwoop, i} = a) when is_integer(i)) do
Testering.Blah.TaggedTuple.Vwoop.empty(a)
end
def(empty(%MyStruct{} = a)) do
Testering.Blah.MineOlStruct.empty(a)
end
def(empty(value)) do
raise(%ProtocolEx.UnimplementedProtocolEx{proto: Testering.Blah, name: :empty, arity: 1, value: value})
end
def(succ(i = a) when is_integer(i)) do
Testering.Blah.Integer.succ(a)
end
def(succ({Vwoop, i} = a) when is_integer(i)) do
Testering.Blah.TaggedTuple.Vwoop.succ(a)
end
def(succ(%MyStruct{} = a)) do
Testering.Blah.MineOlStruct.succ(a)
end
def(succ(value)) do
raise(%ProtocolEx.UnimplementedProtocolEx{proto: Testering.Blah, name: :succ, arity: 1, value: value})
end
def(add(i = a, b) when is_integer(i)) do
Testering.Blah.Integer.add(a, b)
end
def(add({Vwoop, i} = a, b) when is_integer(i)) do
Testering.Blah.TaggedTuple.Vwoop.add(a, b)
end
def(add(%MyStruct{} = a, b)) do
Testering.Blah.MineOlStruct.add(a, b)
end
def(add(value, _)) do
raise(%ProtocolEx.UnimplementedProtocolEx{proto: Testering.Blah, name: :add, arity: 2, value: value})
end
def(a_fallback(i = a) when is_integer(i)) do
Testering.Blah.Integer.a_fallback(a)
end
def(a_fallback(a)) do
inspect(a)
end
end
And I’m thinking about being able to inline from the implementations into the protocol as well, but a lot of things to consider there like macro’s and module attributes and so forth, so I might make it opt-in since it will really only benefit recursively calling such a protocol anyway.
As mentioned it does not have a consolidation compiler built yet like elixir’s, but that is easy enough to do and since all implementations are submodules of the protocol then it will be easy to find them, I might add a @procotolEx_priority
module attribute to allow the matching order they should follow for the very few use-cases that should actually matter (I.E. it can be ignored 99% of the time).
As usual, this is not really intended for use yet (although it should be perfectly fine and efficient to use), I’m still debating on the design. I’m also playing with an idea of inverting the setup, so you make the implementations as just normal modules, nothing special there, then define the protocol and resolver all at the same time as it checks if the modules fulfill that. A benefit of that style would be a single module could fulfill multiple protocols, but a side effect is less control overall, although it does mean no consolidator is necessary since it is all a single step, and it also means no reconsolidation at run-time too, so I’m debating, I think I’ll keep the current style since it is more powerful.
I’m also considering adding in property checks (ala the type_class
library), that would let you enforce certain contracts that the implementers absolutely must fulfill or it would fail compilation. Is that really in the purview of this library though or should it be in another like type_class
, hmm… Thoughts?
Regardless, it could still use more work, testing, and features before I consider it 1.0.0.