Skip to content

Latest commit

 

History

History
211 lines (153 loc) · 6.13 KB

README.md

File metadata and controls

211 lines (153 loc) · 6.13 KB

ProtocolEx

Extended Protocol library.

Performs matching for protocol implementations instead of being limited to certain base types as in standard Elixir Protocols.

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"},

Usage

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

Setup

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.

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)
end
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)
  end
end

You can cancel running tests on compile by passing --no-protocol-tests to the mix compile command.

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)
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.

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: 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.

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

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

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.