🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Custom Event Behaviour

An event behaviour is a contract that names how a module handles events. Different modules can follow the same contract and react to the same events in different ways.

An event behaviour names init and handle functions

EventHandler needs init and handle_event. The callback list shows both functions.

defmodule EventHandler do
  @callback init() :: term()
  @callback handle_event(term(), term()) :: term()
end

IO.inspect({:init, 0} in EventHandler.behaviour_info(:callbacks))
IO.inspect({:handle_event, 2} in EventHandler.behaviour_info(:callbacks))
IO.inspect(length(EventHandler.behaviour_info(:callbacks)))

One handler counts events

Counter follows EventHandler. It starts at zero and adds one for each tick event.

defmodule EventHandler do
  @callback init() :: term()
  @callback handle_event(term(), term()) :: term()
end

defmodule Counter do
  @behaviour EventHandler
  def init(), do: 0
  def handle_event(:tick, count), do: count + 1
  def handle_event(_event, count), do: count
end

IO.inspect(Enum.reduce([:tick, :tick, :other, :tick], Counter.init(), fn event, count -> Counter.handle_event(event, count) end))

One dispatcher works for any handler

Dispatcher threads events through any handler module. The same events give a different result for each handler.

defmodule EventHandler do
  @callback init() :: term()
  @callback handle_event(term(), term()) :: term()
end

defmodule Counter do
  @behaviour EventHandler
  def init(), do: 0
  def handle_event(:tick, count), do: count + 1
  def handle_event(_event, count), do: count
end

defmodule Collector do
  @behaviour EventHandler
  def init(), do: []
  def handle_event({:named, name}, names), do: names ++ [name]
  def handle_event(_event, names), do: names
end

defmodule Dispatcher do
  def run(handler, events), do: Enum.reduce(events, handler.init(), fn event, state -> handler.handle_event(event, state) end)
end

IO.inspect(Dispatcher.run(Counter, [:tick, :tick]))
IO.inspect(Dispatcher.run(Collector, [{:named, "Ana"}, :tick, {:named, "Bo"}]))

Example

defmodule EventHandler do
  @callback init() :: term()
  @callback handle_event(term(), term()) :: term()
end

defmodule Counter do
  @behaviour EventHandler
  def init(), do: 0
  def handle_event(:tick, count), do: count + 1
  def handle_event(_event, count), do: count
end

defmodule Collector do
  @behaviour EventHandler
  def init(), do: []
  def handle_event({:named, name}, names), do: names ++ [name]
  def handle_event(_event, names), do: names
end

defmodule Dispatcher do
  def run(handler, events), do: Enum.reduce(events, handler.init(), fn event, state -> handler.handle_event(event, state) end)
end

events = [:tick, {:named, "Ana"}, :tick, {:named, "Bo"}, :tick]
IO.inspect(Dispatcher.run(Counter, events))
IO.inspect(Dispatcher.run(Collector, events))

Key points

  • Event contract: an event behaviour names an init function and a handle function for events.
  • Handler: a handler module provides both functions and keeps its own state shape.
  • Dispatch: a dispatcher passes each event in order to a handler and threads the returned state into the next event.
  • Reuse: the same dispatcher works for any handler that follows the event behaviour.
  • State: each handler starts from its own init value, so handlers do not share state.

In this example, EventHandler names init and handle_event, Counter counts ticks, and Collector gathers names. Dispatcher runs the same event list through each handler, so each keeps its own final state.

Note

an event behaviour is a plain behaviour, so the compiler checks its callbacks like any other behaviour.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

Two handlers follow the same event behaviour. A dispatcher runs the same events through both. What is true?