0

I'm attempting to make a macro that will take an input stream and do something different depending on the contents of the first line read, and then read further input. I'm having trouble just having a macro that will take an input stream and read some values from it.

A contrived example:

(defmacro read-and-print (&optional in)
  `(print
    ,(if (string= (read-line in) "greet")
         `(concatenate 'string "hello" (read-line ,in))
         `(read-line ,in))))

(with-input-from-string (in "greet
bob") (read-and-print in))

but even that is producing the following error

 There is no applicable method for the generic function
   #<STANDARD-GENERIC-FUNCTION SB-GRAY:STREAM-READ-LINE (1)>
 when called with arguments
   (IN).
   [Condition of type SB-INT:COMPILED-PROGRAM-ERROR]

The thing that's really baffling me is even changing the function to take a string for the first line isn't working:

(defmacro read-and-print (command &optional in)
  `(print
    ,(if (string= command "greet")
         `(concatenate 'string "hello " (read-line ,in))
         `(read-line ,in))))

(with-input-from-string (in "greet
bob")
  (read-and-print (read-string in) in))

This gives me

 The value
   (READ-LINE IN)
 is not of type
   (OR (VECTOR CHARACTER) (VECTOR NIL) BASE-STRING SYMBOL CHARACTER)
 when binding SB-IMPL::STRING1
   [Condition of type SB-INT:COMPILED-PROGRAM-ERROR]

While this executes completely fine:

(with-input-from-string (in "greet
bob")
  (read-and-print "greet" in))

Is there something special about the with-input-from-string macro that I'm missing? I suspect I'm missing something very obvious about macros, but googling has gotten me nowhere.

kstew
  • 5
  • 2
  • 3
    The input to your macro is symbols (well, list of symbols), not stream. You are *generating* code that will accept streams, not running it (yet) in the macro. – wvxvw Oct 26 '17 at 13:18
  • Your macro is crazy. It wants a stream *object* to be passed in at macro-expansion time. This won't happen unless we (ab)use `eval`. If we get that to happen, then the macro wants to read from the stream at macro-expansion time. If the stream yields a particular string, the macro wants to generate code which has that same stream object embedded as a literal in the code itself. – Kaz Oct 27 '17 at 00:42

2 Answers2

3

What you asked

Macros are a code generation tool. They do not evaluate their arguments.

Your with-input-from-string example works because strings are self-evaluating. If you quite the string literal, you will get an error.

What you should have asked

You do not need a macro here. Use a function instead.

When deciding between a function and a macro, you need to ask yourself:

  • Am I defining a new syntax?
  • Does the code generate more code?

Unless you understand the questions and answer yes, you should be using functions.

See also How does Lisp let you redefine the language itself?

sds
  • 58,617
  • 29
  • 161
  • 278
1

Your macro:

(defmacro read-and-print (&optional in)
  `(print
    ,(if (string= (read-line in) "greet")
         `(concatenate 'string "hello" (read-line ,in))
         `(read-line ,in))))

But ,(if ... in ...) makes no sense, since the value of in typically isn't a stream at macro expansion time, but code (a symbol, an expression, ...). Since your code is not executing and the macro sees the source (and not the values of something which has not been executed yet) you can't do that. You can't also not usefully repair that: It's just the wrong approach. Use a function instead.

Use a function:

CL-USER 17 > (defun read-and-print (&optional in)
               (if (string= (read-line in) "greet")
                   (concatenate 'string "hello" (read-line in))
                 (read-line in)))
READ-AND-PRINT

CL-USER 18 > (with-input-from-string (in "greet
foo
bar")
               (read-and-print in))
"hellofoo"

Using a macro

You still can write it as a macro, but then you need to generate the code so that it runs at runtime, and not at macro-expansion time:

(defmacro read-and-print (&optional in)
  `(print
    (if (string= (read-line ,in) "greet")
        (concatenate 'string "hello" (read-line ,in))
      (read-line ,in))))

Note: one would actually might want to handle that in is not evaluated multiple times.

This macro would give you the advantage that the code is inlined. This macro would give you the disadvantage that the code is inlined.

The function above gives you the advantage that the code typically is not inlined. The function above gives you the advantage that the code can optionally be inlined, by telling the compiler to do so with an inline declaration.

Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346