Рекурсивная макросреда - PullRequest
       6

Рекурсивная макросреда

0 голосов
/ 26 октября 2018

В попытке лучше изучить макросы я поиграл с несколькими простыми примерами, включая воссоздание упрощенного последнего потока. У меня возникают проблемы с пониманием того, почему одна из приведенных ниже версий приводит к переполнению стека, а другая - нет.

;; version one - blows up w/ stack overflow
(defmacro ->>> [e1 & exprs]
  `(if ~exprs
       (->>> ~(concat (first exprs) (list e1)) ~@(rest exprs))
       ~e1))

;; version two, works just fine
(defmacro ->>> [e1 & exprs]
  (if exprs
       `(->>> ~(concat (first exprs) (list e1)) ~@(rest exprs))
       e1))

Моя первоначальная реакция состояла в том, что это должно быть связано с тем, что в первом примере, хотя полученное расширение выглядит так, как будто оно будет работать нормально, если бы это был обычный код, поскольку это макрос, рекурсивный вызов постоянно расширяется, и если тест никогда не произойдет. Во второй версии тест if выполняется до того, как какой-либо список будет возвращен для оценки во время выполнения, что даст возможность выйти из него.

Однако я не уверен, верна ли эта ментальная модель, потому что следующий пример (Clojure Brave & True) выглядит довольно похоже на первую версию выше и работает нормально:

(defmacro our-and
  ([] true)
  ([x] x)
  ([x & next]
 `(if ~x (our-and ~@next) ~x)))

EDIT : Чтобы уточнить, я имею в виду, что вышеприведенный our-and аналогичен структурно (не семантически) в том, что он возвращает список, который содержит рекурсивный вызов макроса, аналогично первой версии моего потока последняя копия выше.

Ответы [ 2 ]

0 голосов
/ 26 октября 2018

Иногда проще начать с простой функции и векторов данных.Вот пример:

(ns tst.demo.core
  (:use tupelo.core demo.core tupelo.test))

(defn ->>>
  [val & exprs]
  (spyx val)
  (spyx exprs)
  (if (empty? exprs)
    val
    (let [[expr & others] exprs
          >>     (spyx expr)
          >>     (spyx others)
          [fn & args] expr
          >>     (spyx fn)
          >>     (spyx args)
          fncall (concat [fn val] args)
      >> (spyx fncall)
      result (concat ['->>> fncall] others)]
      (spyx result) )))

с выводом:

val => :val
exprs => ([:form1 1 2 3] [:form2 4 5])

expr => [:form1 1 2 3]
others => ([:form2 4 5])

fn => :form1
args => (1 2 3)

fncall => (:form1 :val 1 2 3)
result => (->>> (:form1 :val 1 2 3) [:form2 4 5])

(->>> :val [:form1 1 2 3] [:form2 4 5]) 

     => (->>> (:form1 :val 1 2 3) [:form2 4 5])

Таким образом, вы можете видеть, что он врезал :val в нужное место (стиль в виде нити) и настроендля рекурсивного вызова.Приближаясь к макросу, мы создаем помощник fn:

(defn my-thread-first-impl
  [val & exprs]
  (spyx val)
  (spyx exprs)
  (if (empty? exprs)
    val
    (let [[expr & others] exprs
          >>     (spyx expr)
          >>     (spyx others)
          [fn & args] expr
          >>     (spyx fn)
          >>     (spyx args)
          fncall (concat [fn val] args)
          >>     (spyx fncall)
          result `(my-thread-first-impl ~fncall ~@others)]
      result)))

; (defmacro my-> [forms] )

(dotest
  (spyx (my-thread-first-impl :val
          '(fn-1 1 2 3)
          '(fn-2 4 5) ))

val => :val
exprs => ((fn-1 1 2 3) (fn-2 4 5))
expr => (fn-1 1 2 3)
others => ((fn-2 4 5))
fn => fn-1
args => (1 2 3)
fncall => (fn-1 :val 1 2 3)

  => (tst.demo.core/my-thread-first-impl (fn-1 :val 1 2 3) (fn-2 4 5))

Окончательный вариант с макросами и фиктивными значениями fn

(defn fn-1 [& args]
  (vec (cons :fn-1 args)))
(defn fn-2 [& args]
  (vec (cons :fn-2 args)))

(defn my-thread-first-impl
  [val & exprs]
  (spyx val)
  (spyx exprs)
  (if (empty? exprs)
    val
    (let [[expr & others] exprs
          >>     (spyx expr)
          >>     (spyx others)
          [fn & args] expr
          >>     (spyx fn)
          >>     (spyx args)
          fncall (concat [fn val] args)
          >>     (spyx fncall)
          result `(my-> ~fncall ~@others)]
      result)))

(defmacro my->
  [& forms]
  (apply my-thread-first-impl forms))

& result:

(my-> :val
  (fn-1 1 2 3)
  (fn-2 4 5))

 => [:fn-2 [:fn-1 :val 1 2 3] 4 5]
0 голосов
/ 26 октября 2018

Ваша ментальная модель верна. Это может помочь представить макрос как функцию, которая принимает код и возвращает код и выполняется во время компиляции. Это должно прояснить различия между первым примером и our-and.

В первом примере у нас есть функция, которая принимает код, а всегда код возврата, который использует макрос ->>>, что приводит к бесконечному расширению макроса. Помните, что выражение if в указанном коде будет оцениваться при времени выполнения , но вы получите переполнение стека при времени компиляции , когда произойдет оценка макроса.

В our-and у нас есть функция, которая имеет три предложения. В двух предложениях, оцененных первыми, возвращается код, который не содержит самого себя. В третьем предложении он возвращает код, содержащий себя. Это делает его похожим на пример 2, а не пример 1.

...