+--[edmz (c)2024]--+
|  .o=o..          |
| . *B.o .o        |
|  + *B.. o        |
|  o               |
| o  just a  Blog  |
| .                |
| o  o             |
| .  o . S         |
|                  |
+----[CHACHA]-----+
$ cat "Dynamic function dispatch with Elixir.html"

creation date:
--------

I am creating a small application that acts as a broker that connects to N different services.

Any client that connects to this broker can send data to any service. This is done by sending some JSON data which contains the payload along with the name of the service that should receive it.

My code has one module per service. Each one of those modules knows how to do its thing.

Initially, I had a case where I chose what module I should call based on what the user had sent. Something along these lines:

case service_name do
  "foo" ->
    MyBroker.Services.Alpha
  "bar" ->
    MyBroker.Services.Beta
  "baz" ->
    MyBroker.Services.Gamma
  "pum" ->
    MyBroker.Services.Gamma
  service_name ->
    raise "Invalid service name '#{service_name}'."
end

This works fine for a handful of services. But when I added a new one I had to remember to edit this file.

Also, I added ecto. And it validating that the service name was valid was also something that I should be doing.

So, I needed a way to:

  • have a list of the valid service names
  • dynamically find the module for a given service name

Turns out this is something that you can already introspect with one simple erlang call to :code.all_loaded. This is in part what IEx does with its autocompletion.

So, after calling :code.all_loaded what’s left is to find all the module names that match what I am looking for.

In my case, all the Services implement a specific callback and all are named Bleh.Whatever.XXXXX.

This will return all modules which start with Bleh.Whatever.:

defmodule Bleh do
  def relevant_sub_modules do
    :code.all_loaded
    |> Enum.map(& elem(&1, 0))
    |> Enum.filter(& String.starts_with?(Atom.to_string(&1), "#{__MODULE__}.Whatever."))
  end
end

Then it’s just a matter of finding which module ‘listens’ or ‘accepts’ a specific service name. For that, I each module has to have a function called accepted_names which returns a list of service names it can process. This is enforced by a behaviour.

Here’s the relevant code.

defmodule Bleh do
  @callback accepted_names() :: List.t
  @callback run :: Anything

  def valid_names do
    relevant_sub_modules
    |> Enum.map(fn(mod) -> mod.accepted_names end)
    |> List.flatten
  end

  def relevant_sub_modules do
    :code.all_loaded
    |> Enum.map(& elem(&1, 0))
    |> Enum.filter(& String.starts_with?(Atom.to_string(&1), "#{__MODULE__}.Whatever."))
  end

  def dynamic_dispatch(name) do
    relevant_sub_modules
    |> Enum.find(fn(mod) -> has_name?(mod.accepted_names, name) end)
    |> call_for_module
  end

  def call_for_module(module) do
    module.run
  end

  def has_name?(accepted_names, name) do
    Enum.find(accepted_names, fn(x) -> x == name end)
  end
end


defmodule Bleh.Whatever.Alpha do
  def accepted_names, do: ["foo"]

  def run, do: IO.puts "I am alpha"
end


defmodule Bleh.Whatever.Beta do
  def accepted_names, do: ["bar"]

  def run, do:  IO.puts "I am beta"
end


defmodule Bleh.Whatever.Gamma do
  def accepted_names, do: ["baz", "pum"]

  def run, do:  IO.puts "yeeess gamma"
end

Now, in my ecto schema I can have the following validation:

def changeset(model, :create, params) do
  model
  |> cast(params, @required_fields, @optional_fields)
  |> validate_inclusion(:service_name, Bleh.valid_names)
end

Also, I can make a function call just by knowing the service name:

Bleh.dynamic_dispatch("foo")
# would print "I am alpha"

Notes

I don’t claim this is THE way to do this. It works for me.

If anyone knows a better way to do this or a better name for it, please hit me up on twitter.

EOF

Options:
$ write edmz
$ cd ..
$ · $