Created
February 10, 2023 23:38
-
-
Save ericfreese/b038a0fd53c59bb669ea5e914c262621 to your computer and use it in GitHub Desktop.
Elixir delegated macros
This file contains hidden or 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
# 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("e(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