Arc Forumnew | comments | leaders | submitlogin
Macro names share namespace with function argument names
5 points by jazzdev 5113 days ago | 10 comments
I just realized I can't have a "test" macro:

  (mac test (name actual compare-fn expected) ...)
and then use test as a parameter name in some unrelated fn:

  (def whatever (test xs) (test (car xs) ...))
That seems pretty bad that they share the same namespace. Maybe this is an isolated case. Is there a good rule-of-thumb for avoiding this? Besides "be careful with macro names" or "be careful with parameter names?


7 points by rocketnia 5113 days ago | link

In my code, I've been doing sorta convoluted things to avoid accidental macro-expansion just like what you're talking about:

  (def whatever (row) (row "something"))     ; unsafe
  (def whatever (row) (do.row "something"))  ; safe
  (def whatever (row) (or.row "something"))  ; safe
  ; The "or.row" one is possibly more efficient, since it expands to
  ; "row", but I use "do.row" 'cause I don't want to desensitize myself
  ; to "or".
  
  (def whatever (tab key) (= tab.key "something"))     ; unsafe
  (def whatever (tab key) (= do.tab.key "something"))  ; doesn't work
  (def whatever (tab key) (= or.tab.key "something"))  ; confuses me
  (def whatever (tab key) (= .key.tab "something"))    ; safe
As long as I keep this up, my macros and parameters can conflict all they like, and it doesn't matter. But this approach basically amounts to qualifying the name of each parameter as it's used in function position, so from my point of view, macros and parameters almost don't live in the same namespace.

-----

6 points by aw 5112 days ago | link

If you look at the Arc compiler around line 448 in ac.scm from Arc 3.1, you can see that the first thing it does it is check whether the "fn" in the first position of a "call" form is a macro:

  (define (ac-call fn args env)
    (let ((macfn (ac-macro? fn)))
      (cond (macfn
             (ac-mac-call macfn args env))
that's why an identifier being a macro takes precedence over an identifier being a lexical variable.

But you can easily change it. The "lex?" function will tell you whether an identifier is a lexical variable at that point. So you can change the test in the cond for "macfn" to something like "(and macfn (not (and (symbol? fn) (lex? fn env))))" and an identifier being a lexical will take precedence over an identifier being a macro.

-----

3 points by fallintothis 5113 days ago | link

Is there a good rule-of-thumb for avoiding this?

If there is, I'd like to know. Seems to be a wart: in the functional position, names are interpreted as macros if they were globally bound as such.

  arc> (mac m (x) `(list 'expanded ',x))
  #(tagged mac #<procedure: m>)         
  arc> (def foo (m) m)
  #<procedure: foo>
  arc> (def bar (m) (+ m 1))
  #<procedure: bar>
  arc> (def baz (m index) (m index))
  #<procedure: baz>
  arc> (foo 5)
  5
  arc> (bar 5)
  6
  arc> (baz "abc" 0)
  (expanded index)
There was a halfhearted thread about this at http://arclanguage.org/item?id=9696, if it's any consolation.

-----

2 points by waterhouse 5107 days ago | link

It seems to me that a lexical binding should override a global macro binding, no matter what. I don't see a reason not to do it that way, except possibly that it would make it harder to handle macroexpansion (it adds another special case to macroexpansion, along with quoted/quasiquoted forms and parameter lists and wherever else macros shouldn't be expanded). But we already have a full code-walker in ac.scm, and aw has shown us just where to look and what to do, so this turns out not to be much of a problem.

And it turns out that both Common Lisp and Scheme agree with me, that lexical bindings should override global macro bindings. As we see here (pardon my choice of throwaway function name; it's short and the computer doesn't mind):

  > (define-macro (ass x) ;Scheme
      `(list ,x 2))
  > (let ((ass (lambda (x) (+ x 1))))
      (ass 1))
  2

  * (defmacro ass (x) `(list ,x 1)) ;CL
  ASS
  * (flet ((ass (x) (+ x 1))) (ass 2))
  3
In the above examples, if the calls to 'ass got expanded as macros, then what would be returned is some kind of list, rather than an integer. It's only in Arc that we get:

  arc> (mac ass (x) `(list ,x 1))
  #(tagged mac #<procedure: ass>)
  arc> (let ass (fn (x) (+ x 1)) (ass 2))
  (2 1)
By the way, in order to format the above code (add 2 spaces before each line), I used the following handy little 'clipboard function after copying the interactions with the REPLs. I think pbpaste works on all Unix-like systems:

  (def clipboard () (tostring:system "pbpaste"))
  
  arc> (let u (instring:clipboard) (whilet a readline.u (prn "  " a)))
    > (define (meh ass)
        (ass 1))
    > (meh (lambda (x) (+ x 1)))
    2
  nil

-----

2 points by evanrmurphy 5113 days ago | link

Your 'whatever function rebinds 'test only for the lexical scope of the function definition. After defining the macro and then the function as you have, 'test is still bound to the macro:

  arc> (mac test (name actual compare-fn expected)
         "test macro")
  #(tagged mac #<procedure: test>)
  arc> (def whatever (test xs)
         test)
  #<procedure: whatever>
  arc> test
  #(tagged mac #<procedure: test>)
  arc> (test nil nil nil nil)
  "test macro"
The binding of function parameters behaves like 'let in this regard, affecting a variable's value within the lexical scope of the block but producing no side effects on it outside.

-----

2 points by evanrmurphy 5113 days ago | link

We can even pass 'test as a parameter to 'whatever and it will retain its original value as a macro, because the rebinding of 'test as a parameter was only for the function's definition, not for a call to that function:

  arc> (whatever test nil)
  #(tagged mac #<procedure: test>)

-----

2 points by evanrmurphy 5113 days ago | link

I think this response misunderstands exactly the trouble you were talking about. Sorry if I confused matters.

-----

1 point by evanrmurphy 5112 days ago | link

Maybe there's a workaround involving quotes. Borrowing from fallintothis' example, 'baz interprets 'm as the macro:

  arc> (mac m (x) `(list 'expanded ',x))
  #(tagged mac #<procedure: m>)
  arc> (def baz (m index) (m index))
  #<procedure: baz>
  arc> (baz "abc" 0)
  (expanded index)
The following 'baz1 interprets it as the parameter:

  arc> (def baz1 (m index) `(,m ,index))
  #<procedure: baz1>
  arc> (baz1 "abc" 0)
  ("abc" 0)
Are there problems with this approach?

-----

2 points by rocketnia 5112 days ago | link

I thought the point of 'baz was to call the first parameter with the second parameter. That is, the result of (baz "abc" 0) would be the character #\a if not for that meddling macro. Your 'baz1 is totally different.

BTW, here's my version of 'baz: (def baz (m index) do.m.index)

-----

1 point by evanrmurphy 5112 days ago | link

Yes, you are right. My 'baz1 doesn't work as intended.

-----