Macros and Genserver - Is it possible to avoid having to define init and start_link functions?

Is it possible to avoid having to define init and start_link functions? Basically I have a few modules that will all share the same parts, but also have specific bits as well. I was hoping I could do something like this:

defmodule Base do
  defmacro __using__(_opts) do
    quote do
      use GenServer
      import Base

      @something null
    end
  end

  def start_link() do
    hydrate_agg() # callback
    # ... stuff that is common
  end

  def init() do
    # ... stuff that is common across everything
  end
end 
defmodule Alpha do
  use Base

  @something "that"
  
  def hydrate_agg do
    #stuff
  end
  # ... callbacks
end
defmodule Beta do
  use Base

  @something "this"

  # ... callbacks
end

But I can’t seem to get it to work, it doesn’t seem to pick up the init or start_link functions. Am I barking up the wrong tree for this?

Define the functions ‘inside’ the macro, like:

defmodule Base do
  defmacro __using__(_opts) do
    quote do
      use GenServer
      import Base

      @something null
  
      def start_link() do
        hydrate_agg() # callback
        # ... stuff that is common
      end

      def init() do
        # ... stuff that is common across everything
      end
    end
  end
end 

That way they will be placed in the using scope. :slight_smile:

2 Likes

Well that makes sense! :grin:
For some reason, I had it in my head that I shouldn’t define functions inside quote do!

Thanks!

1 Like

indeed long quoted blocks with many functions are not a good thing. But this is not the case. Just a bare GenServer bootstrap.

Be careful anyway when hiding implementation details in a macro. It raises the WTF level for newcomers and external developers. Sometimes explicit is better.

2 Likes

A proper way to do this would be by defining a custom behaviour. For example:

defmodule Base do
  @callback hydrate_agg(...) :: ...
  @callback handle_call(...)
  # some more callbacks

  use GenServer

  def start_link(mod, something, opts) do
    GenServer.start_link(__MODULE__, {mod, something}, opts)
  end

  def init({mod, something}) do
    # common stuff
    inner = mod.hydrate_agg(...)
    {:ok, %{inner: inner, mod: mod, something: something})
  end

  def handle_call(msg, from, %{mod: mod, inner: inner} = state) do
    case mod.handle_call(msg, from, inner) do
      {:reply, reply, inner} -> {:reply, reply, %{state | inner: inner}
      # handle other return values
    end
  end
end

This means that later it can be used as:

defmodule Alpha do
  @behaviour Base
  
  def start_link(), do: Base.start_link(__MODULE__, "that", [])
 
  # callbacks
end

defmodule Beta do
  @behaviour Base
  
  def start_link(), do: Base.start_link(__MODULE__, "this", [])

  # callbacks
end

This might be a bit more verbose, but in the long run it will provide more flexibility and better error messages.

3 Likes