Common Lisp: How to build a loop expression with a

2019-07-17 05:49发布

This is a related question, some sort of a follow up.

Let's say I'm trying to build a loop expression by using macros, in which the resulting loop expression is dependant on whether the parameter is a list:

(defmacro testing-loop (var)
   `(eval (append '(loop for x from 0 to 5)
            (when (consp ,var) '(and y in ,var))            
            '(collect)            
            (if (consp ,var) '(y) '(x))))

This seems to work:

CL-USER> (testing-loop 2)
(0 1 2 3 4 5)
CL-USER> (testing-loop (list 5 6 7))
(5 6 7)

But when applying this macro in a lexical closure, it breaks down:

CL-USER> (let ((bar (list 1 2 3)))
           (testing-loop bar))

which throws undefined variable: BAR

I expected testing-loop to macroexpand into the lexical scope where bar is bound?

1条回答
干净又极端
2楼-- · 2019-07-17 06:08

@mck, I see why you want to use eval now. But it's a very messy solution, and slow, as I mentioned in my answer to the previous question. The classic On Lisp says this about eval:

"Generally it is not a good idea to call eval at runtime, for two reasons:

  1. It’s inefficient: eval is handed a raw list, and either has to compile it on the spot, or evaluate it in an interpreter. Either way is slower than compiling the code beforehand, and just calling it.

  2. It’s less powerful, because the expression is evaluated with no lexical context. Among other things, this means that you can’t refer to ordinary variables visible outside the expression being evaluated.

Usually, calling eval explicitly is like buying something in an airport gift-shop. Having waited till the last moment, you have to pay high prices for a limited selection of second-rate goods."

In this case the simplest thing is just to:

(defmacro testing-loop (var)
  (let ((g (gensym)))
   `(let ((,g ,var))
      (if (consp ,g)
        (loop for x from 0 to 5 collect x)
        (loop for x from 0 to 5 and y in ,g collect y)))))

I know you want to factor out the common loop for x from 0 to 5 (which isn't actually needed in the second branch anyways). But loop is itself a macro which is converted at compile time to efficient, low level code. So the call to loop has to be built at compile time, using values which are available at compile time. You can't just insert an (if) in there which is intended to be evaluated at run time.

If you really don't want to repeat loop for x from 0 to 5, you could do something like:

(let ((a '(loop for x from 0 to 5)))
  `(if (consp ,var)
       (,@a collect x)
       (,@a and y in ,var collect y)))

That's just to give you the idea; if you really do this, make sure to gensym!

A good lesson to learn from this is: when you are writing macros, you need to keep clearly in mind what is happening at compile time and what is happening at run time. The macro you wrote with eval compiles the loop macro dynamically, every time it is run, based on the return value of consp. You really want to compile the 2 different loop macros once, and just select the correct one at run-time.

查看更多
登录 后发表回答