2017-01-27 3 views
1

Hier ändern kann ist das, was ich versuche zu tun:einen DSL mit verschachtelten Elixir Makros erstellen, den Block Kontext

%Cake{ 
    name: "Chocolate", 
    topping: %Topping{ 
    name: "Butter cream", 
    sweetener: "Agave" 
    } 
} 
:

defmodule ArbitraryContext do 
    use Cake 

    def make_cake do 
    cake do 
     name "Chocolate" 

     topping do 
     name "Butter cream" 
     sweetener "Agave" 
     end 
    end 
    end 
end 

Ich mag würde ArbitraryContext.make_cake/0 zu verschachtelten Strukturen entlang der Linien zu erzeugen

Ich habe Metaprogramming Elixir und einige andere Ressourcen gelesen, aber ich kann nicht in der Lage sein, einige der DSL-Flexibilität zu reproduzieren, die ich in Ruby in Elixir gewohnt bin. Das scheint falsch, denn Elixir scheint grundsätzlich flexibler zu sein.

Ich habe mit dem "HTML DSL" Beispiel in Metaprogramming Elixir gefolgt. Das HTML-Beispiel ist wesentlich einfacher, da nur ein Modul im Spiel ist - ein Tag -, so dass sein Kontext während der gesamten Verschachtelung gleich bleiben kann. In meinem Fall könnte es Dutzende von Kontexten geben. Ich injiziere Cake-Makros in den Kontext, der die %Cake{...} mit einem Namen erfolgreich produziert, aber wenn der Block für Topping nicht quoted ist, um %Topping{...} zu produzieren, ist der Kontext immer noch Cake. Egal, was ich mache, ich finde keinen sauberen Weg, diesen Block in einem neuen Kontext zu betreiben.

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 ich dies wie Topping.class_eval mit etwas behandeln würde ... Sie mit name und sweetener von Topping und auf der anderen Seite würden am Ende würden Sie mit einem neuen Topping Klasseninstanz enden.

kann ich lösen, wohl viel sauberer, nur durch Gebäude die Strukturen vor-geschachtelt ohne DSL und alle Makros, aber ich möchte verstehen, wie das gewünschte Ergebnis mit Elixir-Makros zu bekommen.

Ich hoffe, ich habe diese Frage gut genug kommuniziert!

+0

Können Sie die Quelle von "Topping" veröffentlichen? Ich denke du willst 'Topping.topping (do: unquote (block))' aber da du die komplette Quelle nicht gepostet hast, kann ich es nicht versuchen. – Dogbert

+0

@Dogbert Der Topping-Code ist nicht wirklich so wichtig oder anders. Das Beispiel, das Sie angegeben haben, hebt den Block in Cakes Topping-Makro auf, bevor das Topping-Makro erreicht wird. Aber ja, auf hoher Ebene, wenn die beiden APIs die gleichen wären, würden Sie das Argument als Keyword-Liste mit "do" formatieren. – onyxrev

+1

Oh. Ich denke du willst das Makro außerhalb des Zitats dann aufrufen, wie: 'defmacro topping (block) topping.topping (block) end'. Dies nennt "Topping.topping/1" mit dem zitierten AST und injiziert den zitierten AST zurück an die Stelle, die 'Cake.topping/1' genannt wird. – Dogbert

Antwort

1

Ich glaube, Sie versuchen, das Meer mit einer Eisenbahnlokomotive zu erobern. Obwohl es immer noch möglich ist, das zu erreichen, was du willst, ist es absolut falsch, elixier zu sein, was auch immer es bedeutet.

An erster Stelle gibt es keine Vorstellung von "Kontext." Überhaupt. Alles, was Sie haben, sind einfach nur alte gute Funktionen. Es gibt zwei Kontexte, wenn Sie darauf bestehen, das Wort "context" zu verwenden: Kompilierung und Laufzeit.

Elixir-Makros sind mehr wie C/C++ - Makros, aber in der gleichen Sprache wie der Hauptcode geschrieben, was Sie wahrscheinlich verwirrt hat. Sie werden während der Kompilierungsphase ausgeführt.

Makros kehren ein einfaches AST zurück, das an Ort und Stelle eingebettet werden soll.

Das heißt, wenn Sie einen Makro erklären:

defmacro cake(do: block), do: block 

Sie einen Strahl am Ende mit (kompilierten Code,), das die Makros alle inlined hat. Egal, wo sie erklärt wurden. Das ist es. Sie könnten noch Makros structs zu produzieren, selbstverständlich, Makros sind immer noch einfach nur AST:

iex> quote do: %{name: "cake", topping: %{name: "blah"}} 
{:%{}, [], [name: "cake", topping: {:%{}, [], [name: "blah"]}]} 

Sobald, wie Ihr Makro gibt Ihrer zitierte Darstellung struct, z.B.genau was quote do wird dafür zeigen, wird es funktionieren. Z.B.

iex> defmodule A do 
...> defmacro cake(toppling), 
...>  do: {:%{}, [], [name: "cake", topping: {:%{}, [], [name: toppling]}]} 
...> def check, do: IO.inspect A.cake("CREAM") 
...> end 

{:module, A, 
<<70, 79, 82, ...>>, {:check, 0}} 

iex> A.check 
%{name: "cake", topping: %{name: "CREAM"}} 

Sie könnten mit dieser Technik zu erreichen, was Sie wollen, aber es macht nicht viel Sinn, da die gesamte Struktur erzeugt wird, kann in Zukunft nicht mehr verändert werden. Begriffe sind unveränderlich, erinnere dich daran.

Hoffe es klärt die Dinge. Fühlen Sie sich frei, weitere Fragen zu stellen, wenn Sie noch neugierig sind.

+0

Wie empfehlen Sie, sich diesem statt Makros zu nähern? – Un3qual

+0

@ Un3qual Ich würde nicht empfehlen, dies überhaupt anzugehen. Wenn Sie immer noch das Bedürfnis haben, die eigene DSL zu implementieren, sind die Makros der richtige Weg. – mudasobwa

0

Ich habe das funktioniert dank Tipps von @dogbert und @mudasobwa. Wie erwartet, ist es grob und chaotisch, aber es funktioniert:

Grund DSL:

defmodule ArbitraryContext do 
    def make_cake do 
    use Cake 

    cake do 
     name "Chocolate" 

     topping do 
     name "Butter cream" 
     sweetener "Agave" 
     end 
    end 
    end 
end 

Kuchen:

defmodule Cake do 
    require Topping 
    defstruct name: nil, topping: nil 

    defmacro __using__(_) do 
    quote do 
     import Cake 
    end 
    end 

    defmacro cake(do: block) do 
    quote do 
     {:ok, var!(pid, Cake)} = %Cake{} |> start_state 

     unquote(block) 

     out = get_state(var!(pid, Cake)) 
     :ok = stop_state(var!(pid, Cake)) 
     out 
    end 
    end 

    defmacro topping(do: block) do 
    topping = Macro.escape(
     Topping.topping(do: block) 
    ) 

    quote do 
     put_state(var!(pid, Cake), :topping, unquote(topping)) 
    end 
    end 

    defmacro name(val) do 
    quote do 
     put_state(var!(pid, Cake), :name, unquote(val)) 
    end 
    end 

    def start_state(state), do: Agent.start_link(fn -> state end) 
    def stop_state(pid), do: Agent.stop(pid) 
    def put_state(pid, key, content), do: Agent.update(pid, fn state -> Map.put(state, key, content) end) 
    def get_state(pid), do: Agent.get(pid, &(&1)) 
end 

Garnierung:

defmodule Topping do 
    defstruct name: nil, sweetener: nil 

    def topping(do: block) do 
    {:ok, pid} = %Topping{} |> start_state 

    Topping.run(pid, block) 

    out = get_state(pid) 
    :ok = stop_state(pid) 
    out 
    end 

    def run(pid, {_block, _context, ast}) do 
    Macro.postwalk(ast, fn segment -> 
     run_call(pid, segment) 
    end) 
    end 

    def run(pid, ast), do: ast 

    def run_call(pid, {method, _context, args}) do 
    apply(Topping, method, [pid] ++ args) 
    end 

    def run_call(pid, ast), do: ast 

    def name(pid, val) do 
    put_state(pid, :name, val) 
    end 

    def sweetener(pid, val) do 
    put_state(pid, :sweetener, val) 
    end 

    def start_state(state), do: Agent.start_link(fn -> state end) 
    def stop_state(pid), do: Agent.stop(pid) 
    def put_state(pid, key, content), do: Agent.update(pid, fn state -> Map.put(state, key, content) end) 
    def get_state(pid), do: Agent.get(pid, &(&1)) 
end 

Und schließlich:

iex(1)> ArbitraryContext.make_cake 
%Cake{name: "Chocolate", 
topping: %Topping{name: "Butter cream", sweetener: "Agave"}} 

So viel wie ich die DSL genieße, glaube ich nicht, dass ich schließlich diesen Ansatz verwenden werde.

Ein etwas vernünftigerer Ansatz, den ich auch versucht habe, ist, das Agent-Geschäft aufzugeben und direkt den AST statisch zu analysieren. Am Ende hat sich die Komplexität nicht gelohnt.

Verwandte Themen