I'm not sure all of your criticisms are accurate. For one thing, I find that it is rather rare to provide default callback implementations for most behaviors - while there are exceptions, I think it's generally more common to have a "default" implementation of the behavior as a whole, for example, an in-memory implementation of a cache or data store as a default with other implementations typically hitting external equivalents. You could also place default implementations in the behavior module itself and use defdelegate from the __using__ macro to have the default implementation delegate to the behavior. Error traces are very clear as long as the macro has `location: :keep` set, which is generally always the case.
I don't disagree that having a way to perhaps ease the boilerplate for this pattern would be nice, but I also appreciate the explicit nature of how it works today. I'm not sure that the solution you described makes me feel like it's a clear win. Boilerplate isn't bad by definition, it can lend clarity when the structure of things is more explicit and standard - the more you hide, and the more magic there is, the more opaque it all becomes, and I think makes it harder to really understand. Anyway, I'm interested to hear everyone elses thoughts, but I wanted to chime in with my two cents. Paul On Wed, Nov 15, 2017 at 10:44 PM Christopher Keele <[email protected]> wrote: > 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 > <https://groups.google.com/d/msgid/elixir-lang-core/3701e507-b002-4e05-9b54-c979f2d3275f%40googlegroups.com?utm_medium=email&utm_source=footer> > . > For more options, visit https://groups.google.com/d/optout. > -- 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/CAK%3D%2B-Ttd0ELZRv2gv39pt0kFonubPWM2JSKUXvLpTt4UNtp9ZA%40mail.gmail.com. For more options, visit https://groups.google.com/d/optout.
