fabioticconi

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 :smiley: Elixir is a very interesting language and I’d love to continue working with it.

Marked As Solved

al2o3cr

al2o3cr

Consider the simplest thing that could work: listing the mapping from command to handler atom explicitly.

  @handlers %{
    "foo" => Commands.Foo,
    "bar" => Commands.Bar,
    # etc
  }
  defp parse!(_state, [cmd | opts]) do
    case Map.fetch(@handlers, cmd) do
      {:ok, mod} -> {:ok, {mod, opts}}
      :error -> {:ok, {:echo, "#{cmd}: UNKNOWN_COMMAND"}}
    end
  end

This approach also has logical extension points for useful things:

  • broadening the possible keys of the map to things like Regexes would allow for “partial match” commands
  • broadening the possible values of the map to {module, baked_in_opts} lets one “command module” serve multiple external commands

One 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_term for storing the map - an Agent still forces every access through a single thread.

Also Liked

ityonemo

ityonemo

For the level of dynamicity you seek, the on_load idea is correct.

For idiomacity:. I recommend not doing shenanigans with camelize and string.to_atom. instead, I recommend registering your modules by updating an application env value (this is backed by an ets table, so it is blazing fast). The keys should be stringified final term of Module.split and the value should be the module itself.

Nitpicky: parse should not be parse!

ityonemo

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

ityonemo

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

Last Post!

fabioticconi

fabioticconi OP

Now, final question on the subject (thanks all for your help, by the way): why do I need to remove the _build directory to trigger the @on_load hook? Shouldn’t that trigger when a module is loaded, and not when compiled?

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews