This still requires a way to know that the table has already been initialized … and this, of course, is where a GenServer really helps: start it up and it manages the table over its lifetime, creating it in its init/1
and then the table is cleaned up automatically when the GenServer exits.
However … holding on to a PID outside the GenServer is a bit dangerous: the GenServer could fall over and be restarted by its supervisor, leaving a stale PID. This is a benefit of having a named table … but … one can also provide an API that fetches the PID from the GenServer’s state and then have something like:
def lookup(key), do: :ets.lookup(GenServer.call(__MODULE__, :ets_pid), key)
def init(_), do: {:ok, :ets.new(__MODULE__, [:duplicate_bag, protected])}
def handle_call(:ets_pid, _from, state), do: {:reply, state, state}
The GenServer is storing the ets table’s PID as its state, and returning it on request. The lookup call can still fail, but now the error would be that the GenServer is not running which is probably a bit more clearly diagnostic of the actual error. This does make the GenServer a bottleneck for lookups in that it must do a message roundtrip to get the pid …
… and so we’re back to having a named table:
def lookup(key), do: :ets.lookup(__MODULE__,, key)
def init(_), do: {:ok, :ets.new(__MODULE__, [:duplicate_bag, :named_table, :protected])}
Simpler, less code, fewer bottlenecks…