2016-10-09 60 views
5

Próbuję napisać makro, które mogą być wykorzystywane zarówno w globalnym i zagnieżdżonego sposób, jak w przykładzie:Podania stan kompilacji czas pomiędzy zagnieżdżonych makr w Clojure

;;; global: 
(do-stuff 1) 

;;; nested, within a "with-context" block: 
(with-context {:foo :bar} 
    (do-stuff 2) 
    (do-stuff 3)) 

Przy stosowaniu w zagnieżdżonej sposób, do-stuff powinien mieć dostęp do {:foo :bar} zestawu przez with-context.

byłem w stanie realizować to tak:

(def ^:dynamic *ctx* nil) 

(defmacro with-context [ctx & body] 
    `(binding [*ctx* ~ctx] 
    (do [email protected]))) 

(defmacro do-stuff [v] 
    `(if *ctx* 
    (println "within context" *ctx* ":" ~v) 
    (println "no context:" ~v))) 

Jednak ja już próbuje przesunąć if ciągu do-stuff od wykonywania na czasie kompilacji, bo czy do-stuff jest nazywany od wewnątrz treść with-context lub globalnie jest informacją, która jest już dostępna podczas kompilacji.

Niestety, nie udało się znaleźć rozwiązanie, gdyż zagnieżdżone makra wydają się rozszerzył w wielu seriach „makr”, więc wiązanie *ctx* (jak określono w ciągu with-context) dynamiczny nie jest dostępny już przy do-stuff zostaje rozszerzony. Więc to nie działa:

(def ^:dynamic *ctx* nil) 

(defmacro with-context [ctx & body] 
    (binding [*ctx* ctx] 
    `(do [email protected]))) 

(defmacro do-stuff [v] 
    (if *ctx* 
    `(println "within context" ~*ctx* ":" ~v) 
    `(println "no context:" ~v))) 

Jakieś pomysły, jak to osiągnąć?

Czy moje podejście jest całkowicie szalone i istnieje wzorzec, w jaki sposób przekazywać stan w taki sposób, od jednego makra do zagnieżdżonego?

EDIT:

Ciało with-context powinien być w stanie pracować z dowolnych wyrażeń, nie tylko z do-stuff (lub innym kontekście świadomych funkcji/makr). Więc coś jak to powinno być również możliwe:

(with-context {:foo :bar} 
    (do-stuff 2) 
    (some-arbitrary-function) 
    (do-stuff 3)) 

(Jestem świadomy, że some-arbitrary-function chodzi o skutki uboczne, to może napisać coś do bazy danych, na przykład).

Odpowiedz

4

Gdy kod jest macroexpanded, Clojure computesfixpoint:

(defn macroexpand 
    "Repeatedly calls macroexpand-1 on form until it no longer 
    represents a macro form, then returns it. Note neither 
    macroexpand-1 nor macroexpand expand macros in subforms." 
    {:added "1.0" 
    :static true} 
    [form] 
    (let [ex (macroexpand-1 form)] 
     (if (identical? ex form) 
     form 
     (macroexpand ex)))) 

Wszelkie wiążące można ustalić w trakcie wykonywania makra nie jest bardziej na miejscu po wyjściu z makra (dzieje się to wewnątrz macroexpand-1). Do czasu rozbudowy wewnętrznego makra kontekst już dawno minął.

Można jednak zadzwonić bezpośrednio pod numer macroexpand, w którym to przypadku połączenie będzie nadal skuteczne. Pamiętaj jednak, że w twoim przypadku prawdopodobnie musisz zadzwonić pod numer macroexpand-all. This answer objaśnia różnice między macroexpand i clojure.walk/macroexpand-all: w zasadzie musisz upewnić się, że wszystkie wewnętrzne formy są makroexanded. Kod źródłowy dla macroexpand-all pokazuje how it is implemented.

Tak, można zaimplementować makra w następujący sposób:

(defmacro with-context [ctx form] 
    (binding [*ctx* ctx] 
    (clojure.walk/macroexpand-all form))) 

W takim przypadku, dynamiczne wiązanie powinno być widoczne od wewnątrz wewnętrznych makr.

+0

Fantastyczny, właśnie tego szukałem, dziękuję! Ciekawostka: rzeczywiście próbowałem tego z 'macroexpand' i' macroexpand-1', ale to nie działało. Po prostu nie wiedziałem o 'macroexpand-all'. Wielkie dzięki. Zastanawiam się jednak, czy mój pomysł przekazania takiego stanu między makrami jest koncepcyjnie właściwy. Wywołanie czegoś takiego jak 'macroexpand' z kodu wewnątrz zawsze wydaje się niezręczne. Czy brakuje mi tu idiomu, który rozwiązałby mój problem w lepszy (bardziej idiomatyczny, czystszy) sposób? – Oliver

+0

Nie mam nic przeciwko temu: w końcu funkcja jest dostępna, dzięki czemu można z niej korzystać. Jest to trochę niezwykłe, więc musisz dostarczyć dobrą dokumentację 'with-context',' * ctx * ', a także dowolne makro, które od tego zależy. Jest to forma sprzężenia, ale jeśli jej potrzebujesz, działa. Nie znam niczego "czystszego" niż to. – coredump

2

bym keep it simple. To rozwiązanie pozwala uniknąć stanu w dodatkowej zmiennej *ctx*. Myślę, że to bardziej funkcjonalne podejście.

(defmacro do-stuff 
    ([arg1 context] 
    `(do (prn :arg1 ~arg1 :context ~context)) 
     {:a 4 :b 5}) 
    ([arg1] 
    `(prn :arg1 ~arg1 :no-context))) 

(->> {:a 3 :b 4} 
    (do-stuff 1) 
    (do-stuff 2)) 

wyjściowa:

:arg1 1 :context {:a 3, :b 4} 
:arg1 2 :context {:b 5, :a 4} 
+0

Dzięki. To naprawdę proste i bardziej funkcjonalne, masz rację. Działa to jednak tylko przy użyciu 'do-stuff' (lub innych funkcji/makr kontekstowych) w bloku makr wątków. Co jeśli chciałbym móc korzystać z dowolnych funkcji/makr w tym ciele, które nie akceptują kontekstu jako ostatniego parametru? Na przykład. '(- >> {: a 3: b 4} (do-stuff 1) (with-open [r stream] ...))'. Jakieś pomysły? (Niestety, moje oryginalne pytanie w istocie nie wspomniało, że jest to wymóg, ponieważ starałem się, aby przykłady były tak proste, jak to tylko możliwe). – Oliver

1

jest jeszcze jeden wariant, aby to zrobić, stosując pewne makro magii:

(defmacro with-context [ctx & body] 
    (let [ctx (eval ctx)] 
    `(let [~'&ctx ~ctx] 
     (binding [*ctx* ~ctx] 
     (do [email protected]))))) 

w tej definicji wprowadzamy inny let wiążące dla ctx. System makro Clojure następnie umieściłby go w zmiennej &env, dostępnej przez wewnętrzne makra podczas kompilacji. Zauważ, że zachowujemy także bindings, aby mogły z niego korzystać wewnętrzne funkcje.

teraz musimy zdefiniować funkcję, aby uzyskać wartość kontekstowego z makra &env:

(defn env-ctx [env] 
    (some-> env ('&ctx) .init .eval)) 

a następnie można łatwo określić do-stuff:

(defmacro do-stuff [v] 
    (if-let [ctx (env-ctx &env)] 
    `(println "within context" ~ctx ":" ~v) 
    `(println "no context:" ~v))) 

w repl:

user> (defn my-fun [] 
     (println "context in fn is: " *ctx*)) 
#'user/my-fun 

user> (defmacro my-macro [] 
     `(do-stuff 100)) 
#'user/my-macro 

user> (with-context {:a 10 :b 20} 
     (do-stuff 1) 
     (my-fun) 
     (my-macro) 
     (do-stuff 2)) 
;;within context {:a 10, :b 20} : 1 
;;context in fn is: {:a 10, :b 20} 
;;within context {:a 10, :b 20} : 100 
;;within context {:a 10, :b 20} : 2 
nil 

user> (do (do-stuff 1) 
      (my-fun) 
      (my-macro) 
      (do-stuff 2)) 
;;no context: 1 
;;context in fn is: nil 
;;no context: 100 
;;no context: 2 
nil 
+0

Dzięki. Jest to bardzo interesujące rozwiązanie, nieco bardziej zaangażowane. Zastanawiam się tylko, czy to dobry pomysł, aby polegać na implementacji wartości 'i env' (zakładając, że istnieją metody takie jak' .init' i '.eval'). Niemniej jednak daje to ciekawy wgląd w to, jak możesz skorzystać z 'i env' w makrach. – Oliver