Subscribe to access all episodes. View plans →
Published June 29, 2020
Elixir 1.10
We have an Elixir application that has a single module named Teacher
, which implements the GenServer
behavoir.
If you’re not familiar with GenServer
s check out episode #12 where go over how they work.
What this GenServer
does is simply adds and subtracts from a running total that starts at 0. Let’s quickly test it out to see that it works.
We’ll start up the GenServer
with Teacher.start_link
. Then with the pid
we can get the total, add to that total, and then subtract from it.
$ iex -S mix
Interactive Elixir (1.10.3)
> {:ok, pid} = Teacher.start_link()
{:ok, #PID<0.151.0>}
> Teacher.total(pid)
0
> Teacher.add(pid, 5)
5
> Teacher.subtract(pid, 2)
3
Now if we we open the Teacher
module it’s is a pretty straightforward module, but it can still take a minute to discern what functions implement the GenServer
callbacks and which ones don’t.
Elixir 1.5 introduced a solution this problem. You can mark what functions in a module are implementations of a callback using the @impl
attribute.
This makes it easier to see what functions implement callbacks when you first look at a module. Let’s go ahead and update our module to use the @impl
attribute. To start we’ll add @impl true
above our init
callback.
lib/teacher.ex
...
@impl true
def init(state) do
{:ok, state}
end
...
Now if we start our project we get a warning. When we add @impl true
to one callback, Elixir will tell us to implement them for the other callbacks in the module for consistency.
$ iex -S mix
warning: module attribute @impl was not set for function handle_call/3 callback (specified in GenServer). This either means you forgot to add the "@impl true" annotation before the definition or that you are accidentally overriding this callback
lib/teacher.ex:26: Teacher (module)
>
So let’s go back to our module add the @impl
attribute to all of our callbacks.
lib/teacher.ex
...
@impl true
def init(state) do
{:ok, state}
end
@impl true
def start_link do
GenServer.start_link(__MODULE__, 0)
end
@impl true
def handle_call({:add, num}, _from, sum) do
sum = num + sum
{:reply, sum, sum}
end
@impl true
def handle_call({:subtract, num}, _from, sum) do
sum = sum - num
{:reply, sum, sum}
end
@impl true
def handle_call(:total, _from, sum) do
{:reply, sum, sum}
end
...
Then when we go to start our project again Elixir tells us there’s an error - start_link
is not actually a callback and then it lists the available callbacks. This is a really nice feature - Elixir will error telling us we’ve tried to include the @impl
attribute for a function that doesn’t implement a callback.
$ iex -S mix
Compiling 1 file (.ex)
warning: got "@impl true" for function start_link/0 but no behaviour specifies such callback. The known callbacks are:
* GenServer.code_change/3 (function)
* GenServer.format_status/2 (function)
* GenServer.handle_call/3 (function)
* GenServer.handle_cast/2 (function)
* GenServer.handle_continue/2 (function)
* GenServer.handle_info/2 (function)
* GenServer.init/1 (function)
* GenServer.terminate/2 (function)
lib/teacher.ex:22: Teacher (module)
Interactive Elixir (1.10.3) - press Ctrl+C to exit (type h() ENTER for help)
Let’s remove it from the start_link
function. Then if we start our project again everything works.
$ iex -S mix
Interactive Elixir (1.10.3)
And here we actually don’t need to add the @impl attribute to each handle_call in the group. We only need to include it to the first one.
Now when we need to go back update this module or when another developer needs to make changes, it will be clear which functions implement callbacks, making our code much more readable.
Pedro Assunção
4 years agoCould be worth mentioning that you can
@impl GenServer
to make it explicit that a particular function is implementing a GenServer behaviour. This is useful when you mix behaviours in the same module (like in a LiveView, for instance).