Arc Forumnew | comments | leaders | submitlogin
Wart: combining keyword and rest parameters
3 points by akkartik 5278 days ago | 5 comments
PG: "Most of the operators in Rtml were designed to take keyword parameters, and what a help that turned out to be. If I wanted to add another dimension to the behavior of one of the operators, I could just add a new keyword parameter, and everyone's existing templates would continue to work. A few of the Rtml operators didn't take keyword parameters, because I didn't think I'd ever need to change them, and almost every one I ended up kicking myself about later. If I could go back and start over from scratch, one of the things I'd change would be that I'd make every Rtml operator take keyword parameters." (http://lib.store.yahoo.net/lib/paulgraham/bbnexcerpts.txt)

Rediscovering this quote, one realizes how curious it is that arc doesn't have keyword parameters. You can fudge them using the optional-alist trick (http://arclanguage.com/item?id=629), but that's a hack. For example, it doesn't work in macros where I also want to pass in a body parameter to inline code. Like if I want to add optional keyword parameters to influence rendering in this pagination macro:

  (mac paginate(url numitems . body)
    `(withs (start-index (param req "from")
             end-index (+ start-index ,numitems))
        ,@body
        (nav ,url start-index end-index)))
My current syntax: a :do separator in calls to this macro:

  (paginate '/search' 10
      ;; optional keyword args go here
      nextcopy "Next →" prevcopy "← Prev"
    :do
      (each doc (cut docs start-index end-index)
        (render-doc doc)))
I figure the keyword :do is more readable than an extra set of parens around the optionals or the body.

Here's how I have to rewrite paginate:

  (mac paginate(url numitems . block)
    (let (params body) (kwargs block '(nextcopy "next" prevcopy "prev")) ; <-- defaults for keyword args
       `(withs (start-index (param req "from")
               end-index (+ start-index ,numitems))
          ,@body
          (nav ,url start-index end-index ',params))))
Auxiliary definitions:

  (def list-len(l)
    (if (acons l)
      (len l)
      0))
  (def alist? (l)
    (if (isa l 'cons)
        (all 2 (map list-len l))))
  (def coerce-tab(tab)
    (if
      (isa tab 'table)  tab
      (alist? tab)      (listtab tab)
      (isa tab 'cons)   (listtab:tuples tab)
                        (table)))
  (def merge-tables tables
    (let ans (table)
      (each tab tables
        (maptable (fn(k v) (= ans.k v)) coerce-tab.tab))
      ans))

  (def regroup args
    (let do-idx (pos ':do args)
      (if (and args do-idx)
        (withs (kwtbl (tuples:cut args 0 do-idx)
                body (cdr:cut args do-idx))
          (list kwtbl body))
        (list args))))

  (def kwargs(args-and-body defaults)
    (let (params body) (apply regroup args-and-body)
      (list (merge-tables defaults params) body)))
Question: What do people think of the :do syntax? Can you think of a good syntax to add python-style keyword parameters without compromising rest parameters?


2 points by fallintothis 5278 days ago | link

At a loss for something useful to contribute (sorry), I thought it might help discussion to rewrite the code to be simpler -- people won't need to wrestle as much with the auxiliary definitions that way.

I know you probably meant to use things like merge-tables in the large, but in the example you only use it for a degenerate case that happens to be fill-table. I also assume all the unbound things in the macro are defined elsewhere (i.e., you define nav somewhere, param is bound (is it supposed to be arg?), req is gotten from macro-expansion context, etc.). So, I'm left with

  (mac paginate(url numitems . block)
    (let (params body) (kwargs block 'nextcopy "next" 'prevcopy "prev")
       `(withs (start-index (param req "from")
                end-index   (+ start-index ,numitems))
          ,@body
          (nav ,url start-index end-index ',params))))

  (def kwargs (args-and-body . defaults)
    (let (kws body) (regroup args-and-body)
      (list (fill-table (listtab:pair defaults) kws)
            body)))

  (def regroup (seq)
    (let (kws body) (split seq (or (pos ':do seq) (len seq)))
      (list kws (cdr body))))
On a side note, you might consider changing alist? so that it doesn't cons/compute every length needlessly:

  (def an-alist (l)
    (and (acons l)
         (all (andf acons [is (len _) 2]) l)))

  arc> (do1 nil (= xs (coerce (rand-string 10000) 'cons)))
  nil
  arc> (time (repeat 1000 (alist? xs)))
  time: 11948 msec.
  nil
  arc> (time (repeat 1000 (an-alist xs)))
  time: 8 msec.
  nil

-----

2 points by akkartik 5278 days ago | link

Awesome comments, thanks! I'm new to lisp. Not reading about lisp, but actually hacking on it :)

Yes I was aware I was leaving nav, etc. unspecified, I was trying to evoke say the 'Goooogle' nav line in searches. I've been using 'req' purely as the arg to defops, which wasn't clear at all, sorry.

Thanks for the pointers to fill-table and arg. I just wasn't aware of them.

-----

2 points by akkartik 5278 days ago | link

Speeding up alist? along a different dimension - on really long lists:

  (def pair? (l)
    (and (acons l)
         (acons:cdr l)
         (no:acons:cddr l)))

  (def alist? (l)
    (and (acons l)
         (all pair? l)))
Thanks for the inspiration!

-----

2 points by conanite 5278 days ago | link

The great thing about macros is that they can inspect their arguments and decide what to do with them; this is what anarki's 'def does in order to generate doc-strings. If the first element of the 'body arg is a string, use that to document the function currently being defined. Similarly, we could say if the first element of paginate's 'body arg is, say, a hash, use that hash as keyword args, and use (cdr body) for the body.

Using {...} syntax for the hash, this could even be elegant:

  (paginate '/search' 10
    { nextcopy "Next &rarr;" prevcopy "&larr; Prev" }
    (each doc (cut docs start-index end-index)
      (render-doc doc)))
Without {...} you would have to fall back to some kind of keyword, but even so I would prefer

  (paginate '/search' 10
    (opts nextcopy "Next &rarr;" prevcopy "&larr; Prev" )
    (each doc (cut docs start-index end-index)
      (render-doc doc)))
, in this case 'paginate checks if the first element of its 'body arg is a list beginning with 'opts. Something like the following (completely untested)

  (mac paginage (url numitems . body)
    (let (opts body) (extract-opts body)
      (etc etc)))

  (def extract-opts (body)
    (if (is (caar body) 'opts) 
        (list (cdar body) (cdr body))
        (list nil body)))
Would you consider this compromises the rest parameters?

-----

3 points by akkartik 5278 days ago | link

Yes, I thought about adding a set of parens around either keyword or rest params. The brace syntax sounds decent. I think it works just as well; it's just a question of what syntax people prefer.

You'd still need support for defaults in extract-opts. Complexity-wise I think the two implementations would be equivalent.

Hmm, one benefit of using a separator like :do: In macros that need just keyword args the opts approach would add redundant syntax to each call.

I don't understand the entire state space here, but I used to think arguments should go into rest params by default, and now I think they should go into keyword args. What do you think?

-----