josevalim

josevalim

Creator of Elixir

Behaviours, defoverridable and implementations

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?

Most Liked

michalmuskala

michalmuskala

I think the main reason people are doing crazy macro things around behaviours is that there’s no good guide or example on how to do this.
So when people want to give an interface similar to GenServer with some additional callbacks, they define regular handle_call functions inside __using__ and make things overridable. This has a lot of issues, poor debugability being probably the biggest one.

I think education could go a really long way in here, so I’d like to propose attaching creating a “Write your own behaviour” guide to the proposal.

10
Post #5
NobbZ

NobbZ

I really like the idea, but I think that @impl might get confused with something related to protocols because of Kernel.defimpl/3, Protocol.assert_impl!/2, and Protocol.extract_impls/2 beeing the only things mentioning “impl” in elixir until now. Therefore I’d opt for something like @override as in Java.

josevalim

josevalim

Creator of Elixir

The reason why we chose @impl is precisely because a protocol is implementation of callbacks. So they are not different, they are exactly the same. So you can think defimpl/3 is about a module of individual @impl.

The reason we rejected @override is because @impl true does not only apply to defoverridable but to any Behaviour. Given when implementing a behaviour without defoverridable there is nothing to override, I don’t think @override would be a good match.

In any case, we are open to discuss alternatives to @impl, but I don’t think @override in particular is a good fit.

Yes but after your questions I realize it can be a source of confusion. I will amend the proposal to not do that.

Where Next?

Popular in Proposals Top

josevalim
I am resubmitting the proposal from earlier today with more context and more focus on the important parts. Some concerns and praises stay...
452 18469 123
New
josevalim
Hi everyone, This is a proposal for introducing local accumulators to Elixir. This is another attempt of solving the comprehension probl...
1043 10934 245
New
josevalim
Hi everyone, We are considering deprecating 'charlists' in Elixir in favor of ~c"charlist". In many languages, 'foobar' is equivalent to...
New
josevalim
One of the major differences between running your application as a release and as a Mix project is the differences in configuration. Mix ...
382 18331 108
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
josevalim
Hi everyone, Erlang/OTP 21 comes with two new guards contributed by @michalmuskala: map_get/2 and is_map_key/2. Now we need to discuss h...
New
josevalim
NOTE: this is a focused thread, so we appreciate if everybody stayed on topic. Feel free to comment anything in regards to calendar forma...
New
josevalim
Hi everyone, as you may be aware, we are researching a type system for Elixir. As preparation for my upcoming ElixirConf US talk, I woul...
New
josevalim
UPDATE: This proposal has been retracted. Read the new proposal here: Local accumulators for cleaner comprehensions Hi everyone, This i...
New
michalmuskala
TL;DR: The Elixir Core team is announcing a call for proposals to extend support for time zones in Elixir’s standard library. The reason...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53578 245
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I fore...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement