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 specifiedhandle
while at the same time not knowing or caring whathandle
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.