Last active
August 23, 2024 00:11
-
-
Save christhekeele/917c430a57bc6eccc3227f90928f396c to your computer and use it in GitHub Desktop.
Behaviours with Defaults for Elixir
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Default.Behaviour do | |
@moduledoc """ | |
Creates a behaviour that carries its own default implementation. | |
When used into a behaviour module, when that module in turn is used, all functions | |
defined on it are given to the using module. | |
This allows you to have concrete implementations of the behaviour's default functionality | |
for testing, unlike cramming them all into a __using__ macro. | |
When the behaviour is used all provided functions are correctly annotated with their | |
`@impl` referring to it, and all made overridable. | |
You can provide two options when using the resulting behaviour module: | |
- `:docs` (default: true) will copy over the documentation for each function | |
- `:inline` (default: false) will inline the default function's implementation, | |
instead of just proxying to the implementation on the behaviour module, which is the | |
standard behaviour. | |
Example: | |
```elixir | |
defmodule Custom.Behaviour do | |
use Default.Behaviour | |
@callback foo :: atom | |
@doc "Computes foo, returns bar." | |
def foo, do: :bar | |
end | |
defmodule Fizz do | |
use Custom.Behaviour | |
end | |
defmodule Buzz do | |
use Custom.Behaviour | |
@doc "Computes foo, returns baz." | |
def foo, do: :baz | |
end | |
Fizz.foo #=> :bar | |
Buzz.foo #=> :baz | |
``` | |
""" | |
def __on_definition__(env, kind, name, params, guards, body) do | |
doc = Module.get_attribute(env.module, :doc) | |
Module.put_attribute(env.module, :__functions__, {doc, kind, name, params, guards, body}) | |
end | |
defmacro __before_compile__(_env) do | |
quote do | |
@doc false | |
def __functions__, do: @__functions__ | |
end | |
end | |
defmacro __using__(opts \\ []) do | |
code = Keyword.get(opts, :do) | |
quote do | |
Module.register_attribute(__MODULE__, :__functions__, accumulate: true) | |
@on_definition Default.Behaviour | |
@before_compile Default.Behaviour | |
defmacro __using__(opts \\ []) do | |
docs = Keyword.get(opts, :docs, true) | |
inline = Keyword.get(opts, :inline, false) | |
defaults = for {doc, kind, name, params, guards, body} <- __MODULE__.__functions__ do | |
info = %{module: __MODULE__, kind: kind, docs: docs, inline: inline} | |
Default.Behaviour.compose_default(info, doc, name, params, guards, body) | |
end | |
[ | |
quote(do: @behaviour __MODULE__), | |
defaults, | |
quote(do: defoverridable __MODULE__), | |
unquote(code), | |
] |> List.flatten |> Enum.filter(&(&1)) | |
end | |
end | |
end | |
# Ignore macros | |
def compose_default(%{kind: kind}, _doc, _name, _params, _guards, _body) | |
when not kind in ~w[def defp]a, do: nil | |
# If we are inlining, we may need any and all functions, private ones included | |
def compose_default(%{inline: true} = info, doc, name, params, guards, body) do | |
compose_definition(info, doc, name, params, guards, body) | |
end | |
# Otherwise we are only interested in public functions | |
def compose_default(%{kind: :def, module: module} = info, doc, name, params, guards, _body) do | |
delegate = compose_delegate(module, name, params) | |
[ | |
compose_module_attribute(:impl, module), | |
compose_definition(info, doc, name, params, guards, delegate), | |
] | |
end | |
# Throw away anything else | |
def compose_default(_info, _doc, _name, _params, _guards, _body), do: nil | |
defp compose_delegate(module, name, params) do | |
args = Enum.map(params, fn | |
{:\\, _, [arg, _default]} -> arg | |
arg -> arg | |
end) | |
compose_application(module, name, args) | |
end | |
defp compose_definition(info = %{kind: :def}, doc, name, params, guards, body) do | |
[ | |
compose_docs(info, doc), | |
compose_function(:def, name, params, guards, body), | |
] | |
end | |
defp compose_definition(%{kind: :defp}, _doc, name, params, guards, body) do | |
compose_function(:defp, name, params, guards, body) | |
end | |
defp compose_docs(%{docs: false} = info, _doc), do: compose_docs(info, false) | |
defp compose_docs(_info, {_, doc}) when is_binary(doc) do | |
compose_module_attribute(:doc, doc) | |
end | |
defp compose_docs(_info, _doc) do | |
compose_module_attribute(:doc, false) | |
end | |
defp compose_function(:def, name, params, guards, body) do | |
quote do | |
def unquote(compose_definition(name, params, guards)) do | |
unquote(body) | |
end | |
end | |
end | |
defp compose_function(:defp, name, params, guards, body) do | |
quote do | |
defp unquote(compose_definition(name, params, guards)) do | |
unquote(body) | |
end | |
end | |
end | |
defp compose_definition(name, params, []) do | |
compose_call(name, params) | |
end | |
defp compose_definition(name, params, guards) do | |
Enum.reduce(guards, compose_call(name, params), fn guard, node -> | |
{:when, [], [node, guard]} | |
end) | |
end | |
defp compose_call(name, params) do | |
{name, [], params} | |
end | |
defp compose_module_attribute(attribute, value) do | |
{:@, [], [ | |
{attribute, [], [value]} | |
]} | |
end | |
defp compose_application(module, function, args) do | |
{:apply, [], [module, function, args]} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment