I am using Emacs, Slime, and SBCL. Simplifying a problem that I am facing, suppose I have this function working:
(defun get-answer (x y z)
(format t "Which animal would you like to be: ~s ~s ~s ~%" x y z)
(let ((answer (read-line)))
(cond ((equal answer x) (format t "user picks ~s" x))
((equal answer y) (format t "user picks ~s" y))
((equal answer z) (format t "user picks ~s" z)))))
It works for a fixed number of inputs:
CL-USER> (get-answer "fish" "cat" "owl")
Which animal would you like to be: "fish" "cat" "owl"
fish
user picks "fish"
NIL
I would like to generalize this function writing a variant argument macro that builds a variant number of cond
clauses. Basically, Common Lisp macro would write the cond clause for me :)
For instance, I wish I could call it like:
CL-USER> (get-answer '("pig" "zebra" "lion" "dog" "cat" "shark"))
Or just:
CL-USER> (get-answer '("dog" "cat"))
Either way, it would generate 6 and 2 appropriate cond
clauses, respectively. I tried building something to achieve this goal.
My draft/sketch focusing on the cond
clause part is:
(defmacro macro-get-answer (args)
`(cond
(,@(mapcar (lambda (str body)
(let ((str (first str body))
(body (second str body)))
`((string= answer ,str)
,body)))
args))))
The variable answer
was supposed to hold the value inserted by the user. However, I can't manage to make it work. slime-macroexpand-1
is not being really helpful.
As an error message, I get:
The value
QUOTE
is not of type
LIST
Which is not something that I was expecting. Is there a way to fix this?
Thanks.
CodePudding user response:
I don't think you need a macro. Note that there's a repeating pattern of ((equal answer x) (format t "user picks ~s" x))
, so you should think how to simplify that.
So, this is your function and expected inputs:
(defun get-answer (x y z)
(format t "Which animal would you like to be: ~s ~s ~s ~%" x y z)
(let ((answer (read-line)))
(cond ((equal answer x) (format t "user picks ~s" x))
((equal answer y) (format t "user picks ~s" y))
((equal answer z) (format t "user picks ~s" z)))))
(get-answer '("pig" "zebra" "lion" "dog" "cat" "shark"))
(get-answer '("dog" "cat"))
I'd write something like this:
(defun get-answer (args)
(format t "Which animal would you like to be: ~{~s ~} ~%" args)
(let ((answer (read-line)))
(when (member answer args :test #'string=)
(format t "User picks ~s" answer)
answer)))
Tests:
(get-answer '("pig" "zebra" "lion" "dog" "cat" "shark"))
(get-answer '("dog" "cat"))
Note that your function always returned NIL
, mine returns chosen string or NIL
. After you recieve user input, you probably don't want to discard it immediately.
And if you have string with values, like this:
(get-answer '(("pig" 1) ("zebra" 3) ("lion" 2) ("dog" 8) ("cat" 12) ("shark" 20)))
I'd use find
:
(defun get-answer (args)
(format t "Which animal would you like to be: ~{~s ~} ~%" (mapcar #'first args))
(let* ((answer (read-line))
(match (find answer args :test #'string= :key #'car)))
(when match
(format t "User picks ~s " (first match))
(second match))))
This function returns NIL
or value of chosen animal.
CodePudding user response:
The question is a little bit strange, but maybe the point is just to learn about Common Lisp's macros. One could just calculate the members of the condition in a first place. That means, generate a list of string=
forms. Then just return a list containing the cond
special form and the list of ((string= str) return-value)
.
(defmacro get-answer (answers)
(let ((clauses (mapcar (lambda (str body)
(let ((str (first str body))
(body (second str body)))
`((string= ,str) ,body))) answers)))
`(cond ,@clauses)))
To test macros, one can use macroexpand and macroexpand-1.
> (macroexpand-1 '(get-answer (("test1" 1) ("test2" 2))))
(COND
((STRING= "test1") 1)
((STRING= "test2") 2))
But I don't personally think it is worth a macro. I think there is nothing wrong with the following function:
(defun get-answer (answer)
(cond
((string= answer "dog") 6)
((string= answer "cat") 2)))
CodePudding user response:
There is a QUOTE, because your macro form contains one:
CL-USER 8 > (defmacro macro-get-answer (args) (print args) nil)
MACRO-GET-ANSWER
CL-USER 9 > (macro-get-answer '(1 2 3))
(QUOTE (1 2 3))
NIL
CL-USER 10 > (macro-get-answer (quote (1 2 3)))
(QUOTE (1 2 3))
NIL
Possible solutions:
don't use a quote
remove the quote
CodePudding user response:
As with Martin Půda's answer: this is not a good place where you need a macro. In particular if you want some undetermined number of options, pass a list of them: Common Lisp has extremely good list-handling functions.
In the simple case you give, the function becomes almost trivial:
(defun get-answer (options)
(format t "Which animal would you like to be (from ~{~A~^, ~})? " options)
(let ((selected (car (member (read-line) options :test #'string-equal))))
(format t "picked ~S~%" (or selected "(a bad answer)"))
selected))
If what you want to to is to map a value selected by the user to another value, then what you want is an alist or a plist:
(defun get-answer/map (option-map)
(format t "Which animal would you like to be (from ~{~A~^, ~})? "
(mapcar #'car option-map))
(let ((selected (assoc (read-line) option-map :test #'string-equal)))
(format t "picked ~A~%" (if selected (car selected) "(a bad value)"))
;; Second value tells us if fn succeeded, as first can be NIL
(values (cdr selected) selected)))
The whole purpose of functions like member
and assoc
is to search for entries in lists: there is no use in laboriously reimplementing half-working versions of them by writing lots of cond
clauses, whether or not you do this automatically.
Now, people often say 'but I want to learn macros, so I will start by learning how to rewrite functions as macros'. That is a not where to start: macros are never useful for rewriting functions in this way, and thinking they are is very damaging.
Here's one simple example of a case where this can't work:
(get-answer (read-options-from-file *my-options-file*))
If get-answer
were a macro how is this meant to work? The number of options is not only unknown at macroexpansion time, it is not even knowable, even in principle, because neither the file's name nor its contents can possibly be known at compile time.
Macros are not replacements for functions: they are are functions between languages: a macro compiles a language which is CL ( other macros) the language understood by the macro into a language which is CL ( other macros).
I am slightly struggling to think of a case where a macro might be useful here, but perhaps we can imagine a language which has a with-answer-options
form:
(with-answer-options (a)
("elephant" (explode-world :because-of a))
("fish" (set-fire-to-computer :blaming a)))
Note that this form now looks nothing like a function call: it is instead a little language which slightly extends CL, and which will get mapped to CL by the following macro:
(defmacro with-answer-options ((var &optional (prompt "Pick an answer"))
&body clauses)
(unless (symbolp var)
(error "~S should be a variable" var))
(let ((choices (mapcar #'car clauses)))
(unless (every #'stringp choices)
(error "choices should be (<literal string> form ...)"))
`(let ((,var (progn (format t "~A from ~{~A~^ ~}? "
,prompt ',choices)
(read-line))))
(cond
,@(mapcar (lambda (clause)
(destructuring-bind (val &body body) clause
`((string-equal ,var ',val)
,@body)))
clauses)
(t (error "~A is not one of ~{~A~^ ~}" ,var ',choices))))))
So now:
> (with-answer-options (a "Pick a bad thing")
("elephant" (explode-world :because-of a))
("fish" (set-fire-to-computer :blaming a)))
Pick a bad thing from elephant fish? frog
Error: frog is not one of elephant fish