fabioticconi
Hi all, I come from a relatively brief Erlang background from many years ago, and I’m trying now to think (again) in that distributed way - while learning Elixir, which I prefer at a glance.
In short, I’d like to know what (if any) is the Elixir-way to deal with a pattern I encounter often my side projects: the command pattern.
Assume I have a little TCP server setup (via ranch, in fact) and I want to have a clean way of adding commands to manipulate “global state” (currently, I’m using Mnesia.. but I already feel like this is not well supported in Elixir. A question for another day).
I went the polymorphism way, so each command is a module with behaviour Command which, in itself, defines callbacks. A series of processes run these commands.
However, clearly each string coming from the socket needs to be processed and validated as a Command. This is my attempt:
defp parse!(_, ["quit" | _]) do {:ok, :quit} end
defp parse!(_, ["shutdown" | _]) do {:ok, :shutdown} end
defp parse!(_, ["echo" | opts]) do {:ok, {:echo, Enum.join(opts, " ")}} end
defp parse!(_, cmd) when cmd == [] do {:ok, {:echo, ""}} end
defp parse!(_state, [cmd | opts]) do
module_name = Macro.camelize(cmd)
try do
module = String.to_existing_atom("Elixir.Commands.#{module_name}")
{:ok, {module, opts}}
rescue
_ -> {:ok, {:echo, "#{cmd}: UNKNOWN_COMMAND"}}
end
end
It works, but I think it’s very brittle. So I’m trying meta-magic but I’m not sure I’m going in the right/sensible/idiomatic direction:
defmacro __using__(_opts) do
quote do
@behaviour Command
@on_load :register_command
def register_command() do
Command.register_command(__MODULE__)
end
end
end
This essentially allows me to register a command implementation, when it’s loaded. Again, it works, but I’m not sure it’s the right way.
If you can advise or point me in the right direction, I’d be very grateful
Elixir is a very interesting language and I’d love to continue working with it.
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex











Showing Posts 13 to 4- Show Best Posts
- Show All Posts (oldest first)
- Show All Posts (newest first)
fabioticconi
Now, final question on the subject (thanks all for your help, by the way): why do I need to remove the
_builddirectory to trigger the@on_loadhook? Shouldn’t that trigger when a module is loaded, and not when compiled?fabioticconi
I had discounted this in the beginning, but.. it’s brilliant. It works across compilation because the VM will not be stopped. It’s pretty much exactly my use case and it’s of a crazy simplicity.
Too bad it doesn’t support an
updatemethod but I don’t strictly need it, I’ll keep it flat:{:command, cmd_name}as key to avoid clashes, and the module as value. Brilliant.fabioticconi
I’m trying with ETS tables. There’s something I don’t get.. I create the table in the Command module, as soon as it’s loaded:
This works fine:
The
__using__macro is like this now, with aProcess.sleepto make sure the ETS table is setup at that point:And then in the
register_command, I try to insert:But this, after the 5 seconds, crashes:
How does the table name,
:commands, not refer to an existing ETS table? I don’t get. Is this because it’s happening at compile time and when the compilation finishes, the ETS table dies?fabioticconi
Ah.. nothing. It can’t be updated.
fabioticconi
I’m having trouble with the set_env, as it doesn’t have an
updatemethod so I have race conditions (modules are loaded in parallel withon_loadexecutions, it seems; maybe I can find some way to have on_load block the whole loading until it’s done).So I’m thinking about alternatives. Ets tables etc, would work. But your idea is interesting @al2o3cr - an “attribute map” of the module? I need to try this
Essentially I still prefer to keep commands as separate files, and to register them automatically on load; it just seems cleaner (also works for hot code loading I think, as then the module is re-loaded and overwrites its entry in the command map). But your suggestion might fit nicely in my architecture.
al2o3cr
Consider the simplest thing that could work: listing the mapping from command to handler atom explicitly.
This approach also has logical extension points for useful things:
Regexes would allow for “partial match” commands{module, baked_in_opts}lets one “command module” serve multiple external commandsOne downside is that the command → module mapping can get quite long; consider extracting parts of it to functions and combining them at compile-time to reduce clutter.
Worth looking into
persistent_termfor storing the map - an Agent still forces every access through a single thread.ityonemo
Looks like I was subtly wrong about ! convention:
But I would also say “don’t put a ! just because something can error”; I would say non-bang functions can raise on “programmer fault” (something analogous to :badarg); but should not raise on “user fault”.
ityonemo
Use application.put_env and application.get_env instead of an agent, it doesn’t need to be supervised, you don’t have to worry about it going down, etc.
Functions that end in ! by convention signify that they are a raising equivalent of a function that emits ok/error tuples
smanza
In my use case I have few ten of messages. Each message has it own struct but the serialization, deserialization is perform on the message module as its define the message ID for a given command.
fabioticconi
Thanks! Essentially with the
on_load, I’m not using string.to_atom anymore. I’m actually using an Agent as a key-value store (string → module as you also suggested). Eventually I’ll need a bit of a smarter key-value store (a treemap for example, if I want to support partial matching of command names, eg “rem” instead of “removefile”).Can you explain why? In the “dynamic dispatch” tutorials they used it so I used it too, but it isn’t clear to me what it is or when it should be used instead..