Mox and Protocols?

I’m not sure this is an inescapable conclusion.

Example:

defmodule TupleStorage do

  def init(),
    do: {:value, nil} # the opaque "handle"

  def put({:value, _}, value),
    do: {:value, value}

  def peek({:value, value}),
    do: value

end

defmodule AgentStorage do
  def init() do
    {:ok, pid} = Agent.start(fn -> nil end)
    pid             # the opaque "handle"
  end

  def put(pid, value) do
    Agent.update(pid, fn _ -> value end)
    pid
  end

  def peek(pid),
    do: Agent.get(pid, fn value -> value end)

end

defmodule Demo do
  def demo(storage, handle) do
    handle = apply(storage, :put, [handle, :something])
    apply(storage, :peek, [handle])
  end
end

handle = TupleStorage.init()
IO.inspect(Demo.demo(TupleStorage, handle))

handle = AgentStorage.init()
IO.inspect(Demo.demo(AgentStorage, handle))
$ elixir demo.exs
:something
:something
$ 
  • TupleStorage stores the state in an immutable data structure.
  • AgentStorage stores the state inside a mutable agent (process).
  • Demo.demo can use either by using the common contract. It knows which function names and arguments to use while at the same time simply using the specified handle while at the same time not knowing or caring what handle actually is.
  • Behaviours simply impose these contract constraints during compile time.

So I argue that it’s the shape of your API that is revealing the “implementation details” by accepting a handle but not returning (a potentially new) one - not the data type of handle (which the “client” ultimately doesn’t care about thanks to Elixir being a dynamically typed langauge).

Which leads to the question if there is a way to still use behaviours (rather than protocols) by rethinking the shape of the API.

1 Like