Struct as init argument in supervisor child list

In my latest project, I set up my GenServers with an Options struct that defines what arguments can be given in the supervisor child list. Example:

defmodule ExSpeedGame.Game.ButtonInput do
  use GenServer

  defmodule Options do
    @type t :: %__MODULE__{
            pins: Types.pins(),
            debounce_delay: integer(),
    @enforce_keys [:pins, :debounce_delay, :name]
    defstruct [:pins, :debounce_delay, :name]

This is then used in the start_link function that splits the data to the correct GenServer.start_link arguments.

  @spec start_link(Options.t()) :: :ignore | {:error, any} | {:ok, pid}
  def start_link(%Options{} = opts) do
      %{pins: opts.pins, debounce_delay: opts.debounce_delay},

I do this so that in the supervisor I can list the child like this:

{ButtonInput, %ButtonInput.Options{pins: pins, debounce_delay: debounce_delay, name: ButtonInput}},

What do you think of this style? It’s quite verbose because I don’t have the quick struct macro in use that would reduce typing. What do you use for this case?

Here is the actual code if you want to see the full context (I tried to only take the relevant parts for the post):

I personally think it’s good. I often do the same, but with a keyword list instead of a struct. The struct is a bit more verbose, but the nice thing about it is that it would easily catch misspelling of keys.

I usually type the option keyword list like this:

@spec start_link([opt]) :: GenServer.on_start()
      when opt: {:name,} | {:delay, integer()}

This is good for documentation, but when it comes to detecting mistyped options or wrong types dyalizer is not bulletproof, therefore some validation logic is still needed.

My view is that your way is quite good for internal APIs. In libraries, where the user is supposed to pass those options, I would probably use a more generic data structure like a keyword list, and rather add some validation logic.

Alternatively, a possible way to catch two birds with one stone is to pass the options as a keyword list, but internally turn it into a struct with struct!/2:

defmodule Options do
  @enforce_keys [:name, :delay]
  defstruct [:name, :delay]

@spec start_link([opt]) :: GenServer.on_start()
            when opt: {:name,} | {:delay, integer()}
def start_link(opts) do
  opts = struct!(Options, opts)
  # ... etc.

Then you can call start_link with a keyword list:

MyModule.start_link(delay: 123, name: SomeName)

# Or, in a Supervisor:
{MyModule, delay: 123, name: SomeName}

Sometimes it might not be worth creating a struct and then immediately taking it apart, but if you need validation of keys this could be one way.

1 Like