Hi!
I’m building a system which is intended to run inside an embedded device and should work with “peripherals”: hardware modules. Each device represented as a struct with a common core and specific device configuration options. So I create a module for each device type and use
the common core module like this (it isn’t a real code, just an example I wrote for this question):
defmodule Tool do
@callback check_config(tool :: term()) ::
:ok
| {:error, term()}
defmacro __using__(_) do
quote do
@behaviour unquote(__MODULE__)
defstruct [:id, :name, :config, :attached]
# and other common core struct fields and functions
end
end
end
defmodule Tools.SimpleRelay do
use Tool
def check_config(%SimpleRelay{config: nil} = _tool), do: {:error, :config_required}
def check_config(%SimpleRelay{} = _tool), do: :ok
end
So now I can instantiate SimpleRelay structs and use them.
The next abstraction is an Action - a specific action a Tool can do: like “turn on” for a relay or “do a measurement” for a sensor. Actions have common names but must have per-device type implementation. So the most obvious solution is Protocols: each Action is a protocol, and we can add an implementation for the specific device types:
defprotocol Actions.TurnOn do
def run(tool)
end
defimpl Actions.TurnOn, for: Tools.SimpleRelay do
def run(%Tools.SimpleRelay{attached: true, config: %{pin: pin}}) do
{:ok, pin_ref} = Circuits.GPIO.open(pin, output)
Circuits.GPIO.write(pin_ref, 1)
end
def run(%Tools.SimpleRelay{}), do: {:error, :device_should_be_attached}
end
And here is a question I’ve got. Actions should have some kind of meta information as well: for example, resulting_events/1
gives a list of returning events (which is also might be different for different devices: one sensor can measure temperature only but an another one does temperature, humidity and happiness on Alpha Centauri), or even different options list: some actions can have a config, so it would be run/1
for no config actions and run/2
for the configurable ones. And so on.
So as a result it is necessary to have a lot of different protocol definitions with the same bodies:
defprotocol Actions.TurnOn do
def run(tool)
def resulting_events(tool)
def other_meta(tool)
end
defprotocol Actions.TurnOff do
def run(tool)
def resulting_events(tool)
def other_meta(tool)
end
defprotocol Actions.DoMeasurement do
def run(tool)
def resulting_events(tool)
def other_meta(tool)
end
For the general module I would use use/2
macro. But I can’t get it to work with defprotocol/2
:
iex(1)> defmodule Action do
...(1)> defmacro __using__(_) do
...(1)> quote do
...(1)> def run(tool)
...(1)> end
...(1)> end
...(1)> end
{:module, Action,
<<70, 79, 82, 49, 0, 0, 5, 68, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 136,
0, 0, 0, 13, 13, 69, 108, 105, 120, 105, 114, 46, 65, 99, 116, 105, 111, 110,
8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>, {:__using__, 1}}
iex(2)>
nil
iex(3)> defprotocol Actions.ShowId do
...(3)> use Action
...(3)> end
{:module, Actions.ShowId,
<<70, 79, 82, 49, 0, 0, 17, 100, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 2, 173,
0, 0, 0, 45, 21, 69, 108, 105, 120, 105, 114, 46, 65, 99, 116, 105, 111, 110,
115, 46, 83, 104, 111, 119, 73, 100, 8, ...>>, {:__protocol__, 1}}
iex(4)>
nil
iex(5)> defmodule Tools.SimpleRelay do
...(5)> defstruct [:id]
...(5)>
...(5)> defimpl Actions.ShowId do
...(5)> def run(%Tools.SimpleRelay{id: id}), do: IO.puts(inspect(id))
...(5)> end
...(5)> end
warning: module Actions.ShowId is not a behaviour (in module Actions.ShowId.Tools.SimpleRelay)
iex:8: Actions.ShowId.Tools.SimpleRelay (module)
iex(6)> relay = struct(Tools.SimpleRelay, id: "ALongStringId")
%Tools.SimpleRelay{id: "ALongStringId"}
iex(7)> Actions.ShowId.run(relay)
** (UndefinedFunctionError) function Actions.ShowId.run/1 is undefined or private
Actions.ShowId.run(%Tools.SimpleRelay{id: "ALongStringId"})
I’ve tried to grok Protocol.__protocol__/2
but without success.
So the questions are:
- Is it possible to reach what I want? Is it my lack of knowledge or this doesn’t work intentionally?
- Or do you think I’ve got a wrong path? Could give me an idea on how to do it in another way (I mean, not “in general” but anything as effective and efficient from the development perspective as protocols)?
Thanks!