Arc Forumnew | comments | leaders | submitlogin
Nested Macros
2 points by forwardslash 4350 days ago | 2 comments
In order to get myself more familiar with arc I started writing an Arc CSS generator - https://github.com/markbahnman/arc-css - and I'm getting stumped with implementing nested rules. I get the feeling that I just don't fully grasp macros.

I modeled the library after html.arc and the tags. The main macro is

    (mac css (sel props . nest)
      `(do ,(pr (string sel "{"))
           ,(gen-css-properties props)
           ,(each g nest
             (gen-nested-css g sel props))))
Where gen-nested-css generates the macro code for nested rules

    (def gen-nested-css (rest sel prev)
     `(css ,(string (if sel sel) " " (car rest))
           ,(join prev (cadr rest))
           ,(if (cddr rest) (cddr rest))))
When I debug it with pr statements I see that when I have a nested rule it gets passed correctly into gen-nested-css but nothing comes of the code it generates.


4 points by fallintothis 4348 days ago | link

First, you have to understand what the quasiquote and unquote are doing. It doesn't seem at first glance like you're clear on that, so a mini-lesson:

  arc> 'the-basic-purpose-of-quote-is-to-prevent-a-symbol-from-being-evaluated
  the-basic-purpose-of-quote-is-to-prevent-a-symbol-from-being-evaluated
  arc> '(quote can be used on any expression --- including a list)
  (quote can be used on any expression --- including a list)
  arc> (list 'you 'can 'think 'of ''(a b c) 'as 'shorthand 'for '(list 'a 'b 'c))
  (you can think of (quote (a b c)) as shorthand for (list (quote a) (quote b) (quote c)))
  arc> `quasiquote-is-pretty-much-just-like-quote
  quasiquote-is-pretty-much-just-like-quote
  arc> (list 'so '`(a b c) 'is 'like '(list 'a 'b 'c))
  (so (quasiquote (a b c)) is like (list (quote a) (quote b) (quote c)))
  arc> `(but suppose you want to write (list 'a 'b c) --- where the last element has no quote)
  (but suppose you want to write (list (quote a) (quote b) c) --- where the last element has no quote)
  arc> `(then quasiquote gives you a shorthand by using "unquote")
  (then quasiquote gives you a shorthand by using "unquote")
  arc> `(so `(a b ,c) is like (list 'a 'b c) and `(a ,b c) is like (list 'a b 'c) etc)
  (so (quasiquote (a b (unquote c))) is like (list (quote a) (quote b) c) and (quasiquote (a (unquote b) c)) is like (list (quote a) b (quote c)) etc)
  arc> `(thus this list will have 4 instead of (+ 2 2) right here: ,(+ 2 2))
  (thus this list will have 4 instead of (+ 2 2) right here: 4)
  arc> `(because `(a b ,(+ 2 2)) is like (list 'a 'b (+ 2 2)))
  (because (quasiquote (a b (unquote (+ 2 2)))) is like (list (quote a) (quote b) (+ 2 2)))
  arc> (list 'see? 'a 'b (+ 2 2))
  (see? a b 4)
  arc> 'voila
  voila
With that understood, each isn't doing what you think it is.

  arc> (each x '(1 2 3)
         (prn "\"each\" is used for side-effects on the variable x = " x)
         (prn "but ultimately, it will return nil")
         (prn "we can call pure functions on x")
         (+ x 1)
         (prn "but we won't see anything they return")
         x
         (* x 12)
         (when (is x 3)
           (prn "witness: the next line is the return value of \"each\"!")))
  "each" is used for side-effects on the variable x = 1
  but ultimately, it will return nil
  we can call pure functions on x
  but we won't see anything they return
  "each" is used for side-effects on the variable x = 2
  but ultimately, it will return nil
  we can call pure functions on x
  but we won't see anything they return
  "each" is used for side-effects on the variable x = 3
  but ultimately, it will return nil
  we can call pure functions on x
  but we won't see anything they return
  witness: the next line is the return value of "each"!
  nil
This is because each is a macro that (on a list) expands

  (each x xs
    (do-something x)
    (do-something-else x))
into something like

  ((rfn loop-around (elements)
     (when (acons elements)
       (let x (car elements)
         (do-something x)
         (do-something-else x))
       (loop-around (cdr elements))))
   xs)
You can see that when we get to the end of the list xs, the variable elements is passed in as nil. Because (acons nil) is nil, we don't return anything from the overall when expression. As such, you can be sure that the return value of each on a list is always going to be nil, because that's how it ends its recursive journey.

Thus,

  arc> `(if I "unquote" (each x '(1 2 3) x) into this list I just get ,(each x '(1 2 3) x))
  (if I "unquote" (each x (quote (1 2 3)) x) into this list I just get nil)
What you actually want is to accumulate the results of manipulating each element of the list. That's what map is for:

  arc> (map (fn (x) (+ x 1)) '(1 2 3))
  (2 3 4)
  arc> `(if I "unquote" (map [* _ 12] '(1 2 3)) into this list I get ,(map [* _ 12] '(1 2 3)))
  (if I "unquote" (map (fn (_) (* _ 12)) (quote (1 2 3))) into this list I get (12 24 36))
So, let's take a look at how

  (mac css (sel props . nest)
    `(do ,(pr (string sel "{"))
      ,(gen-css-properties props)
      ,(map [gen-nested-css _ sel props] nest)))
works out. But let's take it slow and just use macex1 to expand the macro by one step---I don't want to get lost in recursion. Because your macro is making calls to pr at macro-expansion time, I'm going to take the liberty of drawing it out even further, so as the output from the prompt doesn't get too confusing:

  arc> (let macro-expansion (macex1 '(css "#foo" (id 5) (".bar" (z-index 2))))
         (prn)
         (ppr macro-expansion)
         (prn))
  #foo{
  (do "#foo{"
      (do (if 5 (pr "id:" 5 ";"))
          (pr "}"))
      ((css "#foo .bar"
            (id 5 z-index 2)
            nil)))
  nil
Now, we get to see the problem of using the unquote versus unquote-splicing (,@). The unquote will just shove its argument into the list. Because we're unquoting another list itself, you can see the result of the call to gen-nested-css gets stuck with an extra layer of parentheses. In the expansion, it's like saying

  arc> ((+ 2 2))
  Error: "Function call on inappropriate object 4 ()"
instead of

  arc> (+ 2 2)
  4
However, if you use unquote-splicing, the elements of the list will get "inlined" into the overall list that the quasiquote is constructing. E.g.,

  arc> `(symbols wrapped in parens because of unquote: ,'(a b c))
  (symbols wrapped in parens because of unquote: (a b c))
  arc> `(symbols without parens because of unquote-splicing: ,@'(a b c))
  (symbols without parens because of unquote-splicing: a b c)
So,

  (mac css (sel props . nest)
    `(do ,(pr (string sel "{"))
      ,(gen-css-properties props)
      ,@(map [gen-nested-css _ sel props] nest)))
gives us

  arc> (let macro-expansion (macex1 '(css "#foo" (id 5) (".bar" (z-index 2))))
         (prn)
         (ppr macro-expansion)
         (prn))
  #foo{
  (do "#foo{"
      (do (if 5 (pr "id:" 5 ";"))
          (pr "}"))
      (css "#foo .bar"
           (id 5 z-index 2)
           nil))
  nil
Now, whether this is the desired expansion at the end of the day is a matter of debugging. Good luck! :)

Recommendation: with these points (I hope) cleared up, look closer at html.arc. If you had modeled your code after it more precisely, you probably wouldn't have had these issues. But hey, that's how we learn.

I catch some resistance sometimes when I suggest learning Arc by reading Arc, but the source code is not only a definitive reference for what functionality Arc provides, it also gives you a wealth of idiomatic code. And I can't think of a better way to answer "how does [some function] work?" than to look up its usually-terse definition.

-----

3 points by zck 4350 days ago | link

At a minimum, gen-nested-css needs to be a macro, not a function.

But also, recursive macros are hard. You need to know whether you'll recurse without looking at the values of variables, because macroexpansion time is before runtime.

I'm not sure your 'each in css works properly.

-----