Arc Forumnew | comments | leaders | submitlogin
Multi-word commands
2 points by akkartik 1809 days ago | 9 comments
I've always been very happy to see Common Lisp's with-open-file shortened to w/infile in arc, and so on. When I built wart[1] I preserved these names. But when I added infix[2] to wart I could no longer support slashes inside symbols. I ended up with names like w_infile. Yuck. This is even worse than replacing the much-loved[3] hyphens with underscores.

What to do? Well, I've been experimenting with a way to replace the underscore with.. spaces. Purely using macros. Basically, I am now[4] defining macros like the following:

  mac2 (with :nils vars ... body)
    `((fn ,vars ,@body))

  mac2 (with :tmpfile f ... body)
    `(let ,f (tmpfile)
       (before (system:join "rm " ,f)
         ,@body))
And now I can call them like so:

  with nils (x y)
    (list x y)     # => (nil nil)
Without breaking the usual semantics of with

  with (x 1 y 2)
    x+y            # => 3
mac2 is meant to be read as "define a two-word macro". It's basically syntactic sugar for the following:

  mac (with $nils ... rest) :case ($nils = 'nils)
    `((fn ,vars ,@body))    
What do people think of this idea? Some thoughts occur to me:

1. This feature bakes compose even more deeply to a first-class macro. In calls like:

  (prn:with nils ..)
There's no way to implement compose as a function anymore without teaching its result about the specializations of its constituents.

But since I define (compose f g) as the macro `(,f (,g ,@$args)), everything works.

2. I'm using keyword args in the definition of multi-word macros to evoke pattern-matching, showing what parts of a call are 'constants'. (Perhaps I need to move to a pattern-matching rather than function-name-based lookup for calls?) However, I can't use keyword symbols in calls lest I cause weird errors later:

  with :nils (x y z)    # Couldn't ever extend 'with' to use a param called 'nils'
    ...
How important does it seem to delineate the constant parts of the call with keyword syms? Is it worth creating a second namespace of sigils (ha![5]) for constant symbols? Or does 'with nils' seem intuitive? I tend to think so since an fexpr lisp weakens the idea of all params being eval'd anyway.

[1] http://github.com/akkartik/wart#readme

[2] http://arclanguage.org/item?id=16775

[3] http://arclanguage.org/item?id=16782

[4] http://github.com/akkartik/wart/tree/c78231915b29ebb80d5e466690c3ea1182435e30

[5] http://arclanguage.org/item?id=17231



2 points by fallintothis 1807 days ago | link

Ah, there's an idea. Never thought to use the technique for avoiding slashed names. It makes sense. When you start writing Lisp macros, you quickly notice that you can use uninterpreted symbols as syntax. I remember way back when (I think probably when I first read Comprehending Monads by Philip Wadler), I toyed with writing a list macro to support not just the usual (list x), but also list comprehensions, like

  (list (+ x 1) for x in xs if (> x 5))
That was a fun one. I don't think I have the code for it lying around anywhere. Not that it'd be hard to write again---it translates straightforwardly into maps & keeps. I suppose the end of this slippery slope would be the loop macro (http://clisp.hg.sourceforge.net/hgweb/clisp/clisp/file/tip/s...). Not necessarily a bad thing, if you aren't eschewing specialized syntax anyway.

Although, if the sole purpose here is to do away with slashed names, the only real place they're used in Arc is to shorten with- to w/.

  arc> (keep [find #\/ (string _)] (keys sig))
  (w/bars w/rlink w/table when-umatch/r w/uniq w/stdin w/stdout w/outstring w/appendfile w/socket w/outfile w/link w/instring w/infile w/link-if)
So, though the mechanism by which it works is more general, you could stay disciplined by focusing on with instead of mac2 stuff. Something like the following, only in wart instead of Arc. :)

  ; Make 'let not rely on 'with, so it won't dispatch on special keywords by
  ; mistake

  (mac let (var val . body)
    `((fn (,var) ,@body) ,val))

  (= special-with* (table))

  (mac with args
    (aif (special-with* (car args))
         (apply it (cdr args))
         (let (parms . body) args
           `((fn ,(map1 car (pair parms))
               ,@body)
             ,@(map1 cadr (pair parms))))))

  (mac withify (keyword parms . body) ; for want of a better name...
    `(= (special-with* ',keyword) (fn ,parms ,@body)))

  (withify uniq (vars . body)
    `((fn ,vars ,@body) ,@(map1 [uniq] vars)))

  (withify tmpfile (f . body)
    `(let ,f (tmpfile)
       (before (system:join "rm " ,f)
         ,@body)))

  arc> (macex '(with (x 1 y 2) (+ x y)))
  ((fn (x y) (+ x y)) 1 2)
  arc> (macex '(with uniq (x y) (do-something-with x y)))
  ((fn (x y) (do-something-with x y)) gs1740 gs1741)
  arc> (macex '(with tmpfile foo (writefile 'stuff foo)))
  ((fn (foo) (before (system:join "rm " foo) (writefile (quote stuff) foo))) (tmpfile))
I'm not sure if that alleviates your keyword argument woes. It makes the definitions of with variants explicit without needing sigils or whatnot (because what else are you going to pass as the first argument to withify?). But, it also doesn't get along so well (aesthetically) with Scheme-style name-goes-in-arglist definitions.

-----

1 point by akkartik 1807 days ago | link

Yes, that feels like a very complete summary of the tradeoffs. I'm surprised I hadn't noticed that what I was doing was similar to the loop macro!

-----

2 points by Pauan 1809 days ago | link

Well, I think you already know my answer, since I think Nulan's syntax system is superior.

But at least you're coming up with new ideas. I can't knock you for that.

---

"I'm using keyword args in the definition of multi-word macros to evoke pattern-matching, showing what parts of a call are 'constants'. (Perhaps I need to move to a pattern-matching rather than function-name-based lookup for calls?)"

Why not just use actual pattern matching for the function arguments, like Nulan does?

I would like to point out that you can still use function-name-based lookup for calls. Then it would only be the arguments of the function that are pattern matched. That's what Nulan does.

Then you could say this:

  mac (with 'nils vars ... body)
    `((fn ,vars ,@body))

  mac (with 'tmpfile f ... body)
    `(let ,f (tmpfile)
       (before (system:join "rm " ,f)
         ,@body))
Notice that I'm using "quote" directly in the argument list, and that I used plain-old "mac", rather than "mac2". This also lets you place the hard-coded symbol anywhere you want:

  mac (with vars 'nils ... body)
    `((fn ,vars ,@body))
Look, ma, no need for "mac3"!

This also lets you pattern match on keywords too, if you wanted to:

  mac (with :nils vars ... body)
    `((fn ,vars ,@body))

  mac (with :tmpfile f ... body)
    `(let ,f (tmpfile)
       (before (system:join "rm " ,f)
         ,@body))
Basically, I don't see the point of having a special "mac2" macro. I'd just use full-blown pattern matching. It's not that hard (after you've bashed your head on the wall for a while trying to understand it, okay maybe it is pretty hard), and it's much much cleaner.

In fact, depending on how you've implemented wart, you could even add in pattern matching in wart itself, so that this:

  mac (foo 'bar)
    ...
Would be macro-expanded to this:

  mac (foo $u) :case ($u = 'bar)
    ...
This is similar to how you already "extend" the functionality of existing stuff.

-----

1 point by akkartik 1809 days ago | link

"Why not just use actual pattern matching for the function arguments, like Nulan does? You can still use function-name-based lookup for calls. Then it would only be the arguments of the function that are pattern matched."

Yeah, the combination of permitting arbitrary things in function position and haskell-style pattern-matching allows us to emulate full-scale pattern matching. I hadn't noticed that before. I wonder if there's a cleaner way to integrate it completely with the operator-precedence parser.

-----

1 point by Pauan 1809 days ago | link

"I wonder if there's a cleaner way to integrate it completely with the operator-precedence parser."

I assume you're talking about Nulan since as far as I know, wart doesn't have a full-blown parser. In which case it works just fine in Nulan, because the parser runs before everything else (including macros).

-----

1 point by akkartik 1809 days ago | link

Yes, Nulan or something near it.

Patterns aren't first-class in haskell. Are they first-class in Nulan? If not, I was thinking aloud that Nulan's parser might be able to subsume patterns somehow.

-----

2 points by Pauan 1809 days ago | link

"Are they first-class in Nulan?"

They might be, once I make them user-customizable. At least, as "first-class" as macros.

---

"If not, I was thinking aloud that Nulan's parser might be able to subsume patterns somehow"

Well, the way I designed it, Nulan's parser is stand-alone: you can run it without any of the other Nulan stuff.

That's because the parser is really very simple. It's only concerned about lists, symbols, numbers, and strings. It has no knowledge of boxes, or macros, or anything like that.

This is by design. Although it would be possible to integrate the parser more closely into Nulan, I like the clean separation of concerns.

Patterns are currently executed during the macro expansion phase, so that's already long after the parser has run. In addition, while the parser operates everywhere, patterns can only be used in certain places.

How they work is pretty simple. Using the not-yet-created $pattern-rule macro:

  $pattern-rule foo -> {_ pat} val body
    'val + pat + body
Let's look at this program:

  (-> (foo 5) 10) 20
We're creating a function "-> (foo 5) 10" and then calling it with the argument 20.

Now, it will call the "foo" pattern with the following arguments: (foo 5), a unique variable, and 10. These represent the pattern, the argument to the function, and the body, respectively. Whatever the pattern returns is used as the body of the function.

After running the pattern, the end result is this:

  (-> u (u + 5 + 10)) 20
Let's look at another custom pattern:

  $pattern-rule and -> {_ @pat} val body
    pat.reduce
      -> x y
        pattern-match y val x
      body
This time we're calling the "pattern-match" function, which allows the pattern matching to be recursive.

Using this system, it's easy to do, for instance, list destructuring:

  $pattern-rule list -> {_ @pat} val body
    w/box i = -1
      pat.reduce
        -> x y
          pattern-match y ('val[,(++ i)]) x
        body
And now this program:

  -> (list a b c)
    prn a b c
Will get translated into this:

  -> u
    | box a = u.0
    | box b = u.1
    | box c = u.2
    | prn a b c
Though, the above "list" pattern doesn't support the @ splicing syntax, which is much more complicated to implement.

-----

1 point by akkartik 1809 days ago | link

Thanks. It sounds like the basic question is: is selective evaluation useful enough to merit using up quotes inside param lists? Because if we didn't have it, we'd be able to use quotes for pattern matching.

-----

1 point by Pauan 1809 days ago | link

I'm not sure what you mean. Wart already uses macro-like vaus so it already uses implicit evaluation rather than explicit like in Kernel. And what else could quote mean in an argument list other than to distinguish variables from literal symbols?

-----