Here's what I'm trying to do:
defmodule ArbitraryContext do
use Cake
def make_cake do
cake do
name "Chocolate"
topping do
name "Butter cream"
sweetener "Agave"
end
end
end
end
I'd like ArbitraryContext.make_cake/0
to produce nested structs along the lines of:
%Cake{
name: "Chocolate",
topping: %Topping{
name: "Butter cream",
sweetener: "Agave"
}
}
I've read Metaprogramming Elixir and quite a few other resources but I can't seem to be able to reproduce some of the DSL flexibility I'm used to in Ruby in Elixir. That seems wrong, as Elixir seems fundamentally more flexible.
I've been following along with the "HTML DSL" example in Metaprogramming Elixir. The HTML example is fundamentally simpler because there is only one module at play - a Tag - so its context can stay the same throughout the nesting. In my case there could be dozens of contexts. I inject Cake macros into the context, which does successfully produce the %Cake{...}
with a name, but when the block for Topping is unquoted to produce %Topping{...}
, the context is still Cake
. No matter what I do, I can't seem to find a clean way to run that block in a new context.
defmodule Cake do
defstruct name: nil
defmacro __using__(_) do
quote do
import Cake
end
end
defmacro cake(do: block) do
quote do
# agent-y stuff to maintain state while building the cake. not
# super important at this time
{:ok, var!(pid, Cake)} = %Cake{} |> start_state
# here's where the cake is no longer a lie and the name is set
unquote(block)
out = get_state(var!(pid, Cake))
:ok = stop_state(var!(pid, Cake))
out
end
end
defmacro topping(block) do
quote do
# oh no! block gets evaluated here. even if I double quote
# block, it still ultimately gets the Cake scope even though I'm
# passing it into Topping, which is very similar to Cake... meant
# to build up a Topping struct.
#
# I want to:
# 1) get block into Topping.topping without unquoting it
# 2) have the block unquoted in Topping's context, once in there
Topping.topping(unquote(block))
end
end
end
In Ruby I'd handle this with something like Topping.class_eval
... you'd end up with name
and sweetener
from Topping
and on the other side you'd end up with a new Topping class instance.
I can solve this, arguably much cleaner, by just building the structs pre-nested without the DSL and all the macros, but I want to understand how to get the intended result using Elixir macros.
I hope I've communicated this question well enough!
Topping
? I think you wantTopping.topping(do: unquote(block))
but since you haven't posted the complete source I can't try it. – Dogbertdefmacro topping(block) do Topping.topping(block) end
. This will callTopping.topping/1
with the quoted AST and inject the quoted AST returned by it into the place that calledCake.topping/1
. – Dogbert