Skip to content

Instantly share code, notes, and snippets.

@christhekeele
Last active April 22, 2024 02:05
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save christhekeele/917c430a57bc6eccc3227f90928f396c to your computer and use it in GitHub Desktop.
Save christhekeele/917c430a57bc6eccc3227f90928f396c to your computer and use it in GitHub Desktop.
Behaviours with Defaults for Elixir
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