Skip to content

Instantly share code, notes, and snippets.

@ericfreese
Created February 10, 2023 23:38
Show Gist options
  • Save ericfreese/b038a0fd53c59bb669ea5e914c262621 to your computer and use it in GitHub Desktop.
Save ericfreese/b038a0fd53c59bb669ea5e914c262621 to your computer and use it in GitHub Desktop.
Elixir delegated macros
# TODO: Doesn't handle delegating to a delegated macro
# TODO: Only works with `require`, not `import`
defmodule DelegatedMacros do
@callback delegated_macro_modules() :: [atom()]
# To be used into a module wanting to call delegated macros
defmacro __using__(from: module) do
quote do
require unquote(module)
unquote(
module
|> Macro.expand(__CALLER__)
|> then(& &1.delegated_macro_modules())
|> Enum.map(&quote(do: require(unquote(&1))))
)
end
end
# To be used into a module delegating macros
defmacro __using__([]) do
quote do
@behaviour unquote(__MODULE__)
Module.register_attribute(__MODULE__, :delegated_macro_modules, accumulate: true)
@before_compile unquote(__MODULE__)
import unquote(__MODULE__), only: [defmacrodelegate: 2]
end
end
# Based on https://github.com/rill-project/delegate/blob/9e284eefe53bf4d3161c03cc68946cac95436c3f/lib/delegate/macro.ex#L52-L69
# But doesn't do the `require` inline in the macro expansion, and instead
# adds the `to` module to a list of things that need to be required for the
# delegated macros to work
defmacro defmacrodelegate(head, opts) do
{name, _ctx, args} = head
as = Keyword.get(opts, :as, name)
to = Keyword.fetch!(opts, :to)
quote do
@delegated_macro_modules unquote(to)
defmacro unquote(as)(unquote_splicing(args)) do
to = unquote(to)
name = unquote(name)
args = unquote(args)
quote do
unquote(to).unquote(name)(unquote_splicing(args))
end
end
end
end
defmacro __before_compile__(_env) do
quote do
@impl true
def delegated_macro_modules() do
@delegated_macro_modules
end
end
end
end
# This is where the macro is defined in some nested module
defmodule Apples.Foo do
defmacro hello(name), do: "hello " <> name
end
# Sibling module can use it directly
defmodule Apples.Bar do
require Apples.Foo
def bar(name) do
if name == "bar" do
Apples.Foo.hello("you are bar")
end
end
end
# Top-level module delegates the macro to the nested module
defmodule Apples do
use DelegatedMacros
defmacrodelegate hello(name), to: Apples.Foo
end
# Sibling top-level module can access the macro through sibling `Apples`
defmodule Oranges do
use DelegatedMacros, from: Apples
import Ecto.Query
def greetings() do
from(
g in "greetings",
where: g.greeting == Apples.hello("orange")
)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment