Skip to content

Commit 38f460e

Browse files
authored
Improve docs for defoverridable and behaviours
1 parent 13baeb5 commit 38f460e

File tree

1 file changed

+56
-27
lines changed

1 file changed

+56
-27
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5611,11 +5611,10 @@ defmodule Kernel do
56115611
and arity, then the overridable ones are discarded. Otherwise, the
56125612
original definitions are used.
56135613
5614-
It is possible for the overridden definition to have a different visibility
5615-
than the original: a public function can be overridden by a private
5616-
function and vice-versa.
5617-
5618-
Macros cannot be overridden as functions and vice-versa.
5614+
It is possible for the overridden definition to have a different
5615+
visibility than the original: a public function can be overridden
5616+
by a private function and vice-versa. Macros cannot be overridden
5617+
as functions and vice-versa.
56195618
56205619
## Example
56215620
@@ -5642,31 +5641,15 @@ defmodule Kernel do
56425641
As seen as in the example above, `super` can be used to call the default
56435642
implementation.
56445643
5645-
> #### Disclaimer {: .tip}
5646-
>
5647-
> Use `defoverridable` with care. If you need to define multiple modules
5648-
> with the same behaviour, it may be best to move the default implementation
5649-
> to the caller, and check if a callback exists via `Code.ensure_loaded?/1` and
5650-
> `function_exported?/3`.
5651-
>
5652-
> For example, in the example above, imagine there is a module that calls the
5653-
> `test/2` function. This module could be defined as such:
5654-
>
5655-
> defmodule CallsTest do
5656-
> def receives_module_and_calls_test(module, x, y) do
5657-
> if Code.ensure_loaded?(module) and function_exported?(module, :test, 2) do
5658-
> module.test(x, y)
5659-
> else
5660-
> x + y
5661-
> end
5662-
> end
5663-
> end
5664-
56655644
## Example with behaviour
56665645
5667-
You can also pass a behaviour to `defoverridable` and it will mark all of the
5668-
callbacks in the behaviour as overridable:
5646+
`defoverridable` is commonly used with behaviours. The behaviours use
5647+
`@callback` definitions to define the general module API and the
5648+
`__using__` callback is used to define default implementations of
5649+
functions, which can then be overridable.
56695650
5651+
For convenience, you can pass a behaviour to `defoverridable` and it
5652+
will mark all of the callbacks in the behaviour as overridable:
56705653
56715654
defmodule Behaviour do
56725655
@callback test(number(), number()) :: number()
@@ -5694,6 +5677,52 @@ defmodule Kernel do
56945677
end
56955678
end
56965679
5680+
> #### Narrow behaviours and entry points {: .tip}
5681+
>
5682+
> When defining behaviours, a general rule of thumb is to define narrow
5683+
> behaviours, with the minumum amount of callbacks, to facilitate maintenance
5684+
> over time. Fewer callbacks minimize the points of contact between different
5685+
> parts of the system and reduces the risk of breaking changes and of different
5686+
> implementations having inconsistent behaviour. However, when using `defoverridable`
5687+
> with behaviours, you may accidentally define broad interfaces as all default
5688+
> behaviour is provided via `defoverridable`. Furthermore, `defoverridable`
5689+
> necessarily relies on meta-programming, which complicates debugging. `super` is
5690+
> also hard to troubleshoot, as it by definition relies on calling an implicitly
5691+
> defined function.
5692+
>
5693+
> A possible alternative to `defoverridable` is to use optional callbacks and
5694+
> move the default implementation to the caller. Then you can check if a callback
5695+
> exists via `Code.ensure_loaded?/1` and `function_exported?/3`. For instance,
5696+
> in the example above, imagine there is a module that calls the `test/2` function.
5697+
> This module could be defined as such:
5698+
>
5699+
> defmodule CallsTest do
5700+
> def receives_module_and_calls_test(module, x, y) do
5701+
> if Code.ensure_loaded?(module) and function_exported?(module, :test, 2) do
5702+
> module.test(x, y)
5703+
> else
5704+
> x + y
5705+
> end
5706+
> end
5707+
> end
5708+
>
5709+
> The downside of the above code is that it must call `Code.ensure_loaded?/1` and
5710+
> `function_exported?/3` on every invocation of the behaviour, which may impact
5711+
> runtime performance. For this reason, this approach works best when the behaviour
5712+
> has an entry point, such as a `init` callback (as seen in `GenServer`), which you
5713+
> invoke once to guarantee the module is loaded, and from that moment, you only need
5714+
> to perform `function_exported?/3` checks.
5715+
>
5716+
> To recap:
5717+
>
5718+
> * Prefer narrow behaviours
5719+
>
5720+
> * If your behaviour has an entry point, consider using optional callbacks
5721+
> followed by `Code.ensure_loaded?/1` and `function_exported?/3` checks
5722+
>
5723+
> * If using `defoverridable`, avoid relying on `super` to trigger the default
5724+
> behaviour, suggesting users to invoke well-defined APIs instead.
5725+
>
56975726
"""
56985727
defmacro defoverridable(keywords_or_behaviour) do
56995728
quote do

0 commit comments

Comments
 (0)