Search code examples
if-statementmacrosconditional-statementscommon-lispvariadic-functions

How to convert this function into a Common Lisp macro that has a variable number of cond clauses?


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.


Solution

  • 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.