Arc Forumnew | comments | leaders | submitlogin
A keyword param implementation
4 points by mpr 3002 days ago | 7 comments
I recently wrote a Tcl proc, iter (http://wiki.tcl.tk/47678), that allows the programmer some control over iteration inside the loop body. It exposes a next command that returns the next element in the list being looped over. next also jumps the iterating variable ahead one. As explained on the tcl wiki page, I am finding iter useful for parsing arguments to a command. In other languages named arguments that can be passed to a command out of place are known as keyword arguments. I realized Arc does not have built in keyword arguments, and figured it would be interesting to see how iter could be written in Arc. What follows is an implementation of keyword parameters in Arc, as well as argument parsing notation analogous to what I am using in Tcl. Main funcs/macs to check out below are ndef and argparse, both of which are supported by iter.

Some comments to start a discussion: My keyword arguments have to be passed as quoted symbols, like this (f 'key1 val1 'key2 val2). In Tcl we can just pass words around like this [f key1 $val1 key2 $val2]. I know Arc's typing won't allow us to use plain words like that, but it would be nice to have something other than quotes. Colons maybe? @akkartik, I see your keyword args use colons. I'll check that out and see if I can merge the idea into my system.

    (mac iter (var list . body)
      (let i (uniq)
        `(if (is nil ,list)
           nil
           (withs (,i 0
                   ,var (,list ,i)
                   next (fn ()
                          (++ ,i)
                          (= ,var (,list ,i))))
             (while (< ,i (len ,list))
               ,@body
               (next))))))
    
    (def 1+ (n)
      (+ 1 n))
    
    (def default-kwvals (argnames)
      (mappend (fn (name) (if (atom name)
                            `(,name nil)
                            `(,(car name) ,@(cdr name))))
               argnames))
    
    ;; Set up a context w/ positional arguments assigned to the correct values
    (mac w/posargs (argnames argsvar . body)
      `(with ,(intersperse nil argnames)
         (= ,@(mappend (fn (name idx) `(,name (,argsvar ,idx)))
                   argnames (range 0 (len argnames))))
         ,@body))
    
    ;; Set up a context w/ keyword arguments assigned to the correct values
    (mac w/kwargs (kwargs argsvar . body)
      (with (arg (uniq)
             justnames (map (fn (name) (if (atom name)
                                         name
                                         (car name)))
                            kwargs))
        `(with ,(default-kwvals kwargs)
           (iter ,arg ,argsvar
             (case ,arg
               ,@(mappend (fn (a) `(,a (= ,a (next)))) justnames)))
           ,@body)))
        
    ;; 'named def' wraps the function body in a context that handles keyword arguments.
    ;; keyword args can have default values if they are given as a list. the car of
    ;; the list will serve as the name, the cdr as the default val.
    (mac ndef (name _args . body)
      (withs (kwstart (pos '--keys _args)
              pargs   (firstn kwstart _args)
              kwargs  (nthcdr (1+ kwstart) _args))
        `(def ,name args
           (w/posargs ,pargs args
             (w/kwargs ,kwargs args
               ,@body)))))
    
    (ndef fullname (msg --keys (first "Abe") (last "Lincoln"))
      (prn first " " last " says " msg))
    
    (fullname "four score and...")
    ;; Abe Lincoln says four score and..."
    
    (fullname "hey, you sass that hoopy frood...?" 'last "Prefect" 'first "Ford")
    ;; => Ford Prefect says hey, you sass that hoopy frood...?
    
    (mac argparse (args . case-body)
      (let arg (uniq)
        `(iter ,arg ,args
           (case ,arg
             ,@case-body))))
    
    (def scrape (addr . args)
      (with (outfile ((tokens addr #\/) -1)
             use-ssl nil)
        (argparse args
          -o   (= outfile (next))
          -ssl (= use-ssl t))
        (prn outfile)
        (prn use-ssl)))
    
    (scrape "www.foo.com/index.html" '-o "foo.html" '-ssl)
    ;; => foo.html
    ;;    t


3 points by Oscar-Belletti 2998 days ago | link

The iter macro doesn't work: when it reaches the last iteration it calls next which increases i to (len list) and then tries to get that element, causing an error.

Edit:

The iter macro has another problem: it evaluates list to many times:

    arc>(= counter 0)
    arc>(iter a (map [do (++ counter) _] '(1 2 3 4)) (pr a))
    1234nil
    arc>(/ counter 4)
    11
Note: to make the iter macro not throw any errors I modified the while:

    (while (< ,i (len ,list))
      (= ,var (,list ,i))
      ,@body
      (++ ,i))))))

-----

3 points by mpr 2960 days ago | link

Thanks for pointing that out. I think I fixed both those problems here.

    (mac iter (var list . body)
      (let i (uniq)
        `(if (is nil ,list)
           nil
           (withs (els ,list
                   ,i 0
                   ,var (els ,i)
                   next (fn () (++ ,i)))
             (while (< ,i (len els))
               (= ,var (els ,i))
               ,@body
               (next))))))

-----

3 points by Oscar-Belletti 2959 days ago | link

Now the next function returns the next index and not the next element. iter still evaluates list two times and els can cause variable capture.

    arc> (mac iter (var list . body)
          (let i (uniq)
            `(if (is nil ,list)
               nil
               (withs (els ,list
                       ,i 0
                       ,var (els ,i)
                       next (fn () (++ ,i)))
                 (while (< ,i (len els))
                   (= ,var (els ,i))
                   ,@body
                   (next))))))
    #(tagged mac #<procedure: iter>)
    arc> (iter a '(1 2 3) (prn a))
    1
    2
    3
    nil
    arc> (iter els '(1 2 3) (prn els))
    Error: "car: contract violation\n  expected: pair?\n  given: 1"
    arc> (iter a '(a b c d) (prn a ", " (next)))
    a, 1
    c, 3
    nil
This should fix the problems:

    (mac iter (var lst . body)
        (w/uniq (i elst)
          `(let ,elst ,lst
            (if (is ,elst nil)
              nil
              (withs (,i 0
                      ,var (,elst ,i)
                      next (fn ()
                             (++ ,i)
                             (= ,var (,elst ,i))))
                (while (< ,i (len ,elst))
                  (= ,var (,elst ,i))
                  ,@body
                  (++ ,i)))))))

-----

3 points by mpr 2959 days ago | link

Thanks again!

-----

2 points by Oscar-Belletti 2958 days ago | link

My pleasure

-----

2 points by mpr 3002 days ago | link

So I'm not sure if the argparse function is completely obsoleted by keyword args, but it certainly is in the use case above. With the keyword arguments I've written, you can pass arbitrary code as the default value. So I've rewritten the scrape function above like this

    (ndef scrape (addr --keys (outfile ((tokens addr #\/) -1)) use-ssl)
      (prn outfile)
      (prn use-ssl))
Yes, there is some funny variable capture business going on here. But I think it's interesting to think about anyway :)

-----

3 points by akkartik 3002 days ago | link

Just a quick note before I take a deeper look this weekend: I copied the ':arg' syntax from Common Lisp. Racket tried to follow that as well, though they've added their own tweaks to it. Smalltalk and Objective-C have a trailing colon. Needless to say, you shouldn't feel constrained by any of these approaches.

-----