Design a library with async callbacks

I have a question about code design in Elixir.

I’m writing a library that works with bluetooth and I want to make it general so developers can customize based on their own use case .

An example behaviour is when a new device is discovered, I want to notify the user’s code that a new device is discovered.

So I have few approaches but I’m not happy with any of them.

1: using message passing between processes

defmodule Bluetooth do
  def start_discovery(handler_pid) do
    #....
    #eventually will send a message to `handler_pid` when a device is discovered
    #ex: send(handler_pid, {:device_discovered, device})
    :ok
  end
end

This looks fine if only I could have a behaviour like this

defmodule BluetoothDiscovery do
  @callback handle_info({:device_discovered, any}, any) :: any
end

This way users that implement the code explicitly know to implement this function.
I can technically use this but looks weird and compiler won’t complain about missing function if I have another handle_info in my module for example:

defmodule MyBluetoothDiscovery do
  use GenServer
  @behaviour BluetoothDiscovery
  def handle_info(:timeout, state) do
  end
end

2: using function callbacks

defmodule Bluetooth do
  def start_discovery(handler_module) do
    #....
    #eventually will call device_discovered function on handler_module
    #ex: apply(handler_module, :device_discovered, [device])
    :ok
  end
end

Now I can define a behaviour and ask users to implement it

defmodule BluetoothDiscovery do
  @callback device_discovered(device) :: atom
end

This time the compiler gives me warning if I missed the implementation

defmodule MyBluetoothDiscovery do
  use GenServer
  @behaviour BluetoothDiscovery
  def device_discovered(device) do
    #handle new device
  end
end

This also looks fine but reminds me of interfaces in OOP land.

Do you have any suggestion?

What I do is:

  • If the user should always handle the callback in their own process so they cannot screw up ‘my’ process in any way (like exceptioning and such) or if they might take a while, then send a message.
  • If the user is generally fast or just want them to handle it inline, faster, or let them send their own message then I use a behaviour.

:slight_smile:

1 Like