Hey all! I'd like to talk about my biggest frustration with Elixir as she 
is wrote: packaging default callback implementations with behaviours.

The Problem

Today this is the almost universally used idiom:

defmodule Custom.Behaviour do
  @callback foo :: atom
  defmacro __using__(_opts \\ []) do
    quote do
      @behaviour unquote(__MODULE__)
      def foo, do: :bar
    end
  end
end

That is, creating a __using__ macro in the behaviour module with a quote 
block that just defines all the default implementations for the callbacks. 
I firmly believe this is a bad pattern:

   - There is no concrete implementation of them so they can only be tested 
   through a layer of indirection, defining new modules within the test suite
   - If the project is a library that provides a behaviour it doesn't 
   itself use, then tests are the earliest opportunity to get useful compiler 
   warnings about those default implementations
   - Errors raised in these default implementations have an inscrutable 
   backtrace
   - The way the default callback functions work have no good place to be 
   documented
   - The default implementation is far from its callback declaration and 
   easy to get out of sync

The last point is remedied by the new @impl annotation but I don't see that 
being used as often as I'd like.

I also rarely see the macro-generated implementations of the callbacks 
provide their own documentation, and often the behaviour's moduledoc 
doesn't go into depth or cover the workings of all default implementations.

Furthermore, this situation is a lot of newcomer's first exposure to 
writing their own macros, and this idiom teaches them the opposite of good 
macro practices 
<http://elixir-lang.github.io/getting-started/meta/macros.html>, such as:

   - not writing them unless necessary (this really is a boilerplate-common 
   scenario)
   - keeping them slim (the macro grows linearly with number of callbacks, 
   bad for wide-surface-area behaviours)
   - having macros make as many calls to local functions as possible so 
   they can be tested (I've never seen this done for this default-behaviour 
   use-case)
   

The Fix

My proposal is to eliminate usage of this pattern, by providing a better 
way of accomplishing the same thing, preferably within core.

That's a whole discussion on its own, but I would also like to suggest what 
I think is a superior pattern for handling this we could use as the 
recommended happy path: defining the default implementation of a callback 
on the behaviour module itself, immediately alongside its callback 
declaration, and creating a __using__ macro that instead generates a 
delegate to this implementation. This has many virtues and fixes all of my 
frustrations:

   - It gives a concrete implementation of these functions so they can 
   issue compiler warnings
   - and be documented
   - and be tested
   - The implementation is alongside its callback definition to stay sync 
   (indeed, the impl's @spec should always mirror the @callback above it)
   - Errors will reference the offending line within the implementation 
   instead of the __using__ macro call-site
   - Using delegates theoretically reduces compiled code size, assuming the 
   behaviour is used more than once and the implementations aren't oneliners

The main downside is that this pattern requires a little more boilerplate 
(doing the delegation), and sadly the delegates themselves are far from 
their implementations and can still easily get out of sync, although they 
are more likely to raise an error in this case.

By creating a macro that does this boilerplate correctly for you, this 
would be an entirely positive change, and if we put it in core, it will 
hopefully supplant the old idiom as people update their source.

This is the heart of my proposal: let's provide a better way to do this out 
of the box, with as little as boilerplate as possible.


One Implementation

To instigate discussion, I have put up the crude implementation of this 
feature I've been toying with in one of my libraries here 
<https://gist.github.com/christhekeele/917c430a57bc6eccc3227f90928f396c>. 
It's a module with a __using__ macro that will generate a __using__ macro 
for your behaviour modules.

It does more manual AST munging than I would like and steers clear of some 
of the harder questions of what to do with types and specs, simply copying 
the documentation over for functions and providing a delegate 
implementation. Ideally it would only do this for functions flagged as 
callbacks within the behaviour module, though my first take simply copies 
over all public functions.

It can be instructed to suppress documentation, and provide inline 
implementations rather than delegates. It sets the @behaviour for you, 
provides the @impl annotation for all functions, and sets them all as 
overridable. The behaviour module can still be used normally with 
@behaviour so applying these default implementations is entirely optional.


Other considerations

My particular approach (having a __using__ generate a __using__ macro for 
your behaviour) is certainly not the only way to go about this; defining 
which functions are default implementations within a behaviour could be 
done with an explicit macro, declaring that you want those default 
implementations inside your using module could be an explicit macro. I 
prefer this solution because it requires the least effort possible, using 
the __using__ mechanism to not even need explicit requiring, and therefore 
is more likely to be adopted.

This technique doesn't work for modules like GenServer that need a 
different API than their callbacks. For most common cases my approach is 
viable but the implementations could be tucked away inside a .Defaults 
namespace or something instead to support variant APIs.

I like the idea of un-deprecating and repurposing the Behaviour module to 
hold this feature, since its moduledocs could absorb the behaviours 
<https://hexdocs.pm/elixir/behaviours.html#content> page, where I think it 
would be easier to find organically in the docs, just because I find the 
pages section often gets overlooked.

One might argue that having both "@behaviour FooBar" and "use FooBar" again 
could be confusing. This feature could be implemented instead as macros 
that take a behaviour module as an argument, appearing in Macro instead, or 
be provided as an unadorned defdefaults or something, though those seem 
less elegant to me and it would complicate the implementation (at least for 
inlining, where having an @on_definition hook in the behaviour module is 
essential).


But, this is all pretty subjective and ancillary to the main point: *should 
we offer a better way to do this out of the box? *It's quite possible I'm 
the only person who is driven crazy by the current vogue idiom.

-- 
You received this message because you are subscribed to the Google Groups 
"elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/elixir-lang-core/3701e507-b002-4e05-9b54-c979f2d3275f%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to