josevalim

josevalim

Creator of Elixir

Hi everyone,

One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function definitions. This is what allowed our GenServer definition to be as simple as:

defmodule MyServer do
  use GenServer
end

Implementation-wise, use GenServer defines functions such as:

def terminate(reason, state) do
  :ok
end

and then mark them as overridable:

defoverridable terminate: 2

As the community grew, defoverridable/1 started to show some flaws in its implementation. Furthermore, the community did not always follow up on best practices, often times marking functions as overridable but without defining a proper Behaviour behind the scenes.

The goal of this proposal is to clarify the existing functionality and propose extensions that will push the community towards best practices.

Using @optional_callbacks

In the example above, we have used defoverridable terminate: 2 to make the definition of the terminate/2 function optional.

However, in some cases, the use of defoverridable seems to be unnecessary. For instance, we provide a default implementation for handle_call/3 and mark it as overridable, but the default implementation simply raises when invoked. That’s counter-intuitive as it would be best to simply not define a default implementation in the first place, truly making the handle_call/3 callback optional.

Luckily, Erlang 18 added support for marking callbacks as optional, which we support on Elixir v1.4. We propose Elixir and libraries to leverage this feature and no longer define default implementations for the handle_* functions and instead mark them as optional.

Instead of the version we have today:

defmodule GenServer do
  @callback handle_call(message, from, state)

  defmacro __using__(_) do
    quote do
      @behaviour GenServer

      def handle_call(_message, _from, _state) do
        raise "handle_call/3 not implemented"
      end

      # ...

      defoverridable handle_call: 3
    end
  end
end

We propose:

defmodule GenServer do
  @callback handle_call(message, from, state)
  @optional_callbacks handle_call: 3

  defmacro __using__(_) do
    quote do
      @behaviour GenServer

      # ...
    end
  end
end

The proposed code is much simpler conceptually since we are using the @optional_callbacks feature instead of defoverridable to correctly mark optional callbacks as optional. defoverridable will still be used for functions such as terminate/2, which are truly required.

For developers using GenServer, no change will be necessary to their code base. The goal is that, by removing unnecessary uses of defoverridable/1, the Elixir code base can lead by example and hopefully push the community to rely less on such tools when they are not necessary.

The @impl annotation

Even with the improvements above, the usage of defoverridable/1 and @optional_callbacks still have one major downside: the lack of warnings for implementation mismatches. For example, imagine that instead of defining handle_call/3, you accidentally define a non-callback handle_call/2. Because handle_call/3 is optional, Elixir won’t emit any warnings, so it may take a while for developers to understand why their handle_call/2 callback is not being invoked.

We plan to solve this issue by introducing the @impl true annotation that will check the following function is the implementation of a behaviour. Therefore, if someone writes a code like this:

@impl true
def handle_call(message, state) do
  ...
end

The Elixir compiler will warn that the current module has no behaviour that requires the handle_call/2 function to be implemented, forcing the developer to correctly define a handle_call/3 function. This is a fantastic tool that will not only help the compiler to emit warnings but will also make the code more readable, as any developer that later uses the codebase will understand the purpose of such function is to be a callback implementation.

The @impl annotation is optional. When @impl true is given, we will also add @doc false unless documentation has been given. We will also support a module name to be given. When a module name is given, Elixir will check the following function is an implementation of a callback in the given behaviour:

@impl GenServer
def handle_call(message, from, state) do
  ...
end

defoverridable with behaviours

While @impl will give more confidence and assistance to developers, it is only useful if developers are defining behaviours for their contracts. Elixir has always advocated that a behaviour must always be defined when a set of functions is marked as overridable but it has never provided any convenience or mechanism to enforce such rules.

Therefore we propose the addition of defoverridable BehaviourName, which will make all of the callbacks in the given behaviour overridable. This will help reduce the duplication between behaviour and defoverridable definitions and push the community towards best practice. Therefore, instead of:

defmodule GenServer do
  defmacro __using__(_) do
    quote do
      @behaviour GenServer
      def init(...) do ... end
      def terminate(..., ...) do ... end
      def code_change(..., ..., ...) do ... end
      defoverridable init: 1, terminate: 2, code_change: 3
    end
  end
end

We propose:

defmodule GenServer do
  defmacro __using__(_) do
    quote do
      @behaviour GenServer
      def init(...) do ... end
      def terminate(..., ...) do ... end
      def code_change(..., ..., ...) do ... end
      defoverridable GenServer
    end
  end
end

By promoting new defoverridable API above, we hope library developers will consistently define behaviours for their overridable functions, also enabling developers to use the @impl true annotation to guarantee the proper callbacks are being implemented.

The existing defoverridable API will continue to work as today and won’t be deprecated.

PS: Notice defoverridable always comes after the function definitions, currently and as well as in this proposal. This is required because Elixir functions have multiple clauses and if the defoverridable came before, we would be unable to know in some cases when the overridable function definition ends and when the user overriding starts. By having defoverridable at the end, this boundary is explicit.

Summing up

This proposal promotes the use the of @optional_callbacks, which is already supported by Elixir, and introduces defoverridable(behaviour_name) which will push library developers to define proper behaviours and callbacks for overridable code.

We also propose the addition of the @impl true or @impl behaviour_name annotation, that will check the following function has been listed as a callback by any behaviour used by the current module.

Feedback?

Showing Posts 35 to 26

samphilipd

samphilipd

I implemented this behaviour as specified in Jose’s original proposal.

Defoverridable now takes module name as argument (merged) - https://github.com/elixir-lang/elixir/pull/6022

@impl warnings (needs feedback) - https://github.com/elixir-lang/elixir/pull/6031

Comments are welcome :slight_smile:

Qqwy

Qqwy

TypeCheck Core Team

Yes, it does! Thank you! :smiley:

josevalim

josevalim OP

Creator of Elixir

The suggestions here should be backwards compatible. For example, imagine we make handle_call/3 an optional callback and we change use GenServer to no longer implement handle_call/3 by default. As long as we change the GenServer implementation to also cope with that, then for the user it makes no difference. If the user code defines its own handle_call/3, then it has nothing to override now and it should just work.

@impl true is not a concern because it applies only to those consuming the behaviour (and not the ones implementing it).

Does it clarify your concerns? :slight_smile:

Qqwy

Qqwy

TypeCheck Core Team

Let me re-ask my earlier question because it got overlooked because of the discussion that sparked here directly afterwards :slight_smile: :


How can we properly migrate to this new way of doing things, while for the time being keeping our code backwards-compatible with older Elixir versions?Use @optional_callbacks but also write defoverridable for backwards compatibility? Or is there a way to switch between these statements at compile-time based on the Elixir version, to make it very explicit that part of it is legacy-support that might be removed in the future?

josevalim

josevalim OP

Creator of Elixir

This proposal has been accepted and moved to the issues tracker under issues #5734 and #5735.

eproxus

eproxus

I think I get it now: If @impl true is used, the function marked will get tested against any existing optional callback and a warning will get issued if none is found. If @impl true is used somewhere, all functions that match an optional callback pattern will get warnings if they themselves don’t use @impl true. Correct me if I’m wrong.

Therefore, if a function exists that does not match the pattern of an optional callback (different name or arity) no warnings would be issued. Which I thought was the case, but I’m not sure from where. I guess it was the combo of “arity mismatch warning” and “other callbacks will get warnings if @impl true is used once”.

christhekeele

christhekeele

Gotcha, I think I just read too much into “extend the defoverridable implementation to point towards best practices”; and read “help backing up your overridable functions with behaviours” as "help backing up your behaviours with default overridable functions.

I think we’re on the same page now, and definitely 100% on this proposal. :thumbsup:

josevalim

josevalim OP

Creator of Elixir

No, we will warn if you put @impl true before a function and there is no callback with that exact name and arity. It is not about a partial match on the name or arity. And once you use @impl true in a given module, we will require all callbacks to be properly tagged as @impl true.

Can you please re-read the original proposal and try to pinpoint which part may have led you to think it is something specific to a given name or arity, so we can further clarify it?

eproxus

eproxus

Maybe I misunderstood, but wasn’t the warning supposed to be about a mismatch in arity? If a behaviour exposes the optional callbacks foo/1 and bar/2, and I put an @impl true in front of my foo/1 and I have a bar/3, what would happen?

josevalim

josevalim OP

Creator of Elixir

That won’t work. Whatever is calling your code, such as a GenServer, can’t know which one you picked. Conflicting behaviours are always going to be an error.

There seems to be a lot of confusion on this thread related to what is a behaviour and what are overridable functions. You can’t derive a behaviour because a behaviour has no default implementations to derive.

And no, that’s not what is being proposed. :slight_smile: Behaviours and protocols are abstract, they don’t provide implementations (and they won’t). This is about making it clear when you are implementing a particular behaviour.

Where Next? Top

Trending in Proposals Top

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews