🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Optional Callbacks

An optional callback is a behaviour function that an implementing module may leave out. Required callbacks must still be defined, while an optional one is provided only when it fits.

A behaviour marks one callback as optional

Worker needs run and may offer describe. The optional list names describe alone.

defmodule Worker do
  @callback run(term()) :: term()
  @callback describe() :: String.t()
  @optional_callbacks describe: 0
end

IO.inspect(Worker.behaviour_info(:optional_callbacks))
IO.inspect({:run, 1} in Worker.behaviour_info(:callbacks))
IO.inspect({:describe, 0} in Worker.behaviour_info(:callbacks))

A minimal module defines only the required callback

MiniWorker defines run and leaves describe out. Its run still works, and the describe check is false.

defmodule Worker do
  @callback run(term()) :: term()
  @callback describe() :: String.t()
  @optional_callbacks describe: 0
end

defmodule MiniWorker do
  @behaviour Worker
  def run(x), do: x + 10
end

IO.inspect(MiniWorker.run(21))
IO.inspect(function_exported?(MiniWorker, :describe, 0))

A caller checks before using an optional function

Helper asks whether describe exists before calling it. A full worker gives its description, and a minimal worker gets a default label.

defmodule Worker do
  @callback run(term()) :: term()
  @callback describe() :: String.t()
  @optional_callbacks describe: 0
end

defmodule FullWorker do
  @behaviour Worker
  def run(x), do: x * 2
  def describe(), do: "full"
end

defmodule MiniWorker do
  @behaviour Worker
  def run(x), do: x + 10
end

defmodule Helper do
  def label(worker) do
    if function_exported?(worker, :describe, 0), do: worker.describe(), else: "no description"
  end
end

IO.inspect(Helper.label(FullWorker))
IO.inspect(Helper.label(MiniWorker))

Example

defmodule Worker do
  @callback run(term()) :: term()
  @callback describe() :: String.t()
  @optional_callbacks describe: 0
end

defmodule FullWorker do
  @behaviour Worker
  def run(x), do: x * 2
  def describe(), do: "full"
end

defmodule MiniWorker do
  @behaviour Worker
  def run(x), do: x + 10
end

defmodule Runner do
  def run_and_label(worker, input) do
    label = if function_exported?(worker, :describe, 0), do: worker.describe(), else: "no description"
    {worker.run(input), label}
  end
end

IO.inspect(Runner.run_and_label(FullWorker, 21))
IO.inspect(Runner.run_and_label(MiniWorker, 21))

Key points

  • Optional list: @optional_callbacks names the callbacks that an implementing module may omit.
  • Required rule: every callback not on the optional list must still be defined.
  • Check: function_exported?/3 reports whether a module defines a named function.
  • Fallback: a caller can use a default value when the optional function is absent.
  • Inspection: behaviour_info with optional_callbacks returns the optional list.

In this example, Worker marks describe as optional, FullWorker defines it, and MiniWorker leaves it out. Runner checks for describe, runs the worker, and pairs each result with its label.

Note

an optional callback still has a @callback definition in the behaviour module.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A behaviour marks describe/0 as optional. A module defines only the required callbacks. What happens at compile time?