What are the benefits, if any, of `@impl true` over `@impl <behaviour>`?

The question in the subject line is pretty self-explanatory, and I’m trying to understand perhaps a hidden misunderstanding. My understanding is that @impl true tells that compiler that the function below it implements a behaviour and will thus check so. Then the behavior of @impl <behaviour> is the same except it additionally checks that the name of the function below it implements a callback of the same name defined by <behaviour>. The documentation also states that false may be passed to @impl, but it does not state what @impl false does. So, I’m not sure what it does do.

@impl docs.

In my experience, Elixir developers tend to only use @impl true, but I honestly cannot understand why. With only a few more characters in most cases, you get an additional check and additional clarity, both in single behaviour and multiple behaviour uses. So, I’m both confused why @impl true is in the language and why developers use it.

So, what are the benefits, if any, of @impl true over @impl <behaviour>? Why should one not always use @impl <behaviour>?

1 Like

I’m a guilty party for using @impl true in older code. I think at that’s how @impl was documented at the time. These days, yes, definitely should be @impl BehaviourModule.

I believe evaluation of @impl follows the normal Elixir rules: everything except nil and false are “truthy”. I think its only more recent Elixir releases that validate the module as being a behaviour and then checking valid callbacks.

6 Likes

I think @impl true was added for convenience. For cases where it is clear which behaviour is used.
For example:

defmodule MyAppError do
  defexception [:message]

  @impl true
  def exception(value) do
    msg = "did not get what was expected, got: #{inspect(value)}"
    %MyAppError{message: msg}
  end
end

In my opinion it is a good idea to use @impl <behaviour. Someone with the same opinion has also written a credo check for it.

7 Likes

In one word, @impl true is a shortcut of @impl <behaviour> when the behaviour implemented by the function is unambiguous.

@impl true just helps you to type less, no other benefits. (in my opinion)

Edit: read @bmitc 's reply, too.


If your module only implements one behaviour, then use @impl true is fine, because the behaviour you are implementing is explicit. For example:

defmodule Demo do
  use GenServer  # it contains `@behaviour GenServer`, checkout https://github.com/elixir-lang/elixir/blob/d4d0523a1278ba2cc5a59023dcf54659fb458873/lib/elixir/lib/gen_server.ex#L749 

  @impl true
  def handle_call ...
end

When your module implements multiple behaviours, it’s better to use explicit name. Or, it will make other developers confused.

defmodule Demo do
   @behaviour B1
   @behaviour B2

   @impl B1
   def fn1 ...
 
   @impl B2
   def fn2 ...
end
6 Likes

It doesn’t seem like I have any technical misunderstanding then.

@impl true only saves five characters in the most common case of GenServer, so I in general question its convenience and utility. Even in the case of a single behaviour being used, I also don’t like having to scroll up to the use section to see what behaviour is being implemented. And if one comes back later and implements a second behaviour, then one needs to (“need to” as in “should”) go back and replace all the @impl true instances. So in general, it is actually less convenient and requires more work.

In the example of the Exception behaviour, the exact name of the module isn’t clear unless one knows the module already (I had to look it up to be sure).

And yes, I certainly already enforce Credo.Check.Readability.ImplTrue. :slight_smile: (The check, to be clear, prevents the use of @impl true.)

7 Likes

That’s actually not true. And the docs point that out. You can use multiple behaviours in a single module and just use @impl true for all the callbacks. The compiler emits no warnings or errors.

4 Likes

Thanks, @bmitc. I have fixed the wrong description.

4 Likes

Why? The only possible results of such a change:

  • nothing happens because you got it right
  • you get a compiler error because you didn’t write the THING THE COMPILER ALREADY KNOWS
1 Like
  1. It’s not necessarily about communicating with the compiler. It’s about providing clarity for your future self and other developers in the code about what callback is being implemented and from what behaviour.

  2. When using @impl true, my understanding is that the compiler only checks that the function implements a callback defined in any behaviour that is used. Thus, the compiler actually does do an extra check when using @impl <behaviour> in that it checks that that specific behaviour defines the implemented callback. So the compiler may know something with @impl true but it doesn’t enforce it. And what about the cases where behaviours have similar or the same callbacks defined?

Certainly @impl <behaviour> provides all the expected behavior a extra clarity at zero extra cost.

6 Likes

While in theory I actually support the points made in favor of @impl <behaviour> I think one also can acknowledge that modules implementing multiple behaviours are actually rather rare. I don’t think I’ve ever seen a module implement multiple behaviours for real.

3 Likes

Where I have used it is when defining a proper GenServer state as a struct and then needing to also implement the Access behaviour. An example.

I have certainly seen it used in other places as well but would need to think where.

3 Likes

I use in ex_cldr_calendars to implement both the Calendar and Cldr.Calendar behaviours. I think thats the only place I use it.

1 Like

Exactly. It’s not so much a technical thing as a people thing.

If we were still writing code so that computers could easily understand it, we’d be writing steaming mounds of unmaintainable spaghetti code in raw binary. (Well okay some of us still do, but it’s widely Considered Harmful.)

Instead, we emphasize making it understandable to the next poor shlemazel who’s going to have to maintain it. Bear in mind thought that that may be ourselves six months later, after we’ve forgotten what we were thinking at the time.

(You may be thinking “isn’t that what comments are for?”, but as some wiseguy said, “a comment is a lie waiting to happen”, if it isn’t a lie as soon as written, plus most comments are really just redundant noise that gets in the way and slows us down. So now we try to write code clear enough not to need comments. They’re often still necessary to explain why we’re doing something, especially why not do it some different way that may be more obvious, but we should avoid needing comments to explain what it’s doing or how.)

So, how that applies here is that when you say

@impl true
def some_func(some_args) do

it may not be not immediately obvious what behavior that callback name is from. Granted, most modules are only using one behavior at most, and I’d guess the majority are probably using a common enough one that we’d recognize the callback name (like handle_cast), but there may be multiple, or it may be a more obscure one, and the function definition may be far enough from the use clause that it’s not convenient to go check. By contrast if we say:

@impl TimeOffForGoodBehavior
def some_func(some_args) do

that tells our hypothetical shlemazel exactly where the callback name comes from, plus it gets us some automagic checking, at the cost of a tiny bit more typing, which will not become a lie.

4 Likes

Perhaps there is another advantage of using @impl <Behaviour>. When we have some tools to view inline documentation it’s easier to jump right into it by simply clicking on behaviour module in @impl <Behaviour> expression.

2 Likes

By the way, what’s the purpose of @impl false? I tried to find explanation in docs, but it’s not clear.

@impl false is just the same as not having @impl at all. If you set a callback to @impl false and others to true, you’ll get a warning that it is not set.

1 Like

It looks like @impl false adds one additional warning.

Suppose:

# This code is saved in sample.exs file
defmodule A do
  @callback blue(integer) :: integer
  @callback green(integer) :: integer
end

defmodule B do
  @behaviour A

  @impl A
  def blue(value), do: value + 1

  def green(value), do: value + 2
end

In this case we have:

❯ elixir sample.exs
warning: module attribute @impl was not set for function green/1 callback (specified in A). This either means you forgot to add the "@impl true" annotation before the definition or that you are accidentally overriding this callback
  sample.exs:12: B (module)

But if we add @impl false for the green/1 function, we get:

❯ elixir sample.exs
warning: got "@impl false" for function green/1 but it is a callback specified in A
  sample.exs:13: B (module)

warning: module attribute @impl was not set for function green/1 callback (specified in A). This either means you forgot to add the "@impl true" annotation before the definition or that you are accidentally overriding this callback
  sample.exs:13: B (module)

So there is one additional warning here.

Oh interesting. Seems then like it’s for those who want to go the extra, extra mile of also flagging what isn’t a callback :person_shrugging: :smiley_cat: