1
votes

Given the following function, entered at the REPL:

(defun animalp (thing)
  (if (member thing '(dog cat snail mouse)) t))

It is fairly simple to ask:

(animalp 'dog)

Things get more complicated after splitting into packages:

(in-package :common-lisp-user)
(defpackage :animalia
  (:use :common-lisp)
  (:export :animalp))

(in-package :animalia)
(defun animalp (thing)
  (if (member thing '(dog cat snail mouse)) t))

Now, one can still ask the same question:

(animalia:animalp 'animalia::dog)

But it is getting messy. (I'm not fussed about "animalia:animalp", it's the large number of animals that is problematic.) Essentially I'd like to be able to ask:

(animalia:animalp 'dog)

The symbols (dog, cat, etc) don't have any properties - essentially I've been using them as a shorthand for string comparisons:

(if (member "dog" '("dog" "cat" "mouse" "snail")
            :test #'string-equal) t)

So my question is around best practice. I'm not enamoured with using strings - especially when (eq 'dog 'dog) is so simple within a single package. On the other hand, neither am I overjoyed with the prospect of (defpackage ... (:export :dog :cat ...)) and needing to qualify each animal with a package (animialia:dog, etc). The final obvious solution is to make all the animals keywords, like:

(if (member :dog '(:dog :cat :mouse :snail)) t)

but even that just seems a little dirty.

What are some best practice solutions to achieve what I want, without making a complete mess or resorting to the evolution of fanciful, ugly and potential fragile solutions?

2

2 Answers

2
votes

Note that string-equal accepts string designators, and that symbols are string designators. You can be very flexible and define animalp in terms of string-equal. This does mean that you're doing string comparison, but it doesn't require you use actual strings in other places in your code.

(defpackage #:animalia
  (:use "COMMON-LISP"))

(in-package #:animalia)

(defun animalp (x)
  (if (member x '(cat dog fish)
              :test 'string-equal)
      t))

(in-package #:cl-user)

(animalia::animalp :fish)            ;=> T  (keyword)
(animalia::animalp 'cl-user::fish)   ;=> T  (non-animalia package)
(animalia::animalp 'animalia::fish)  ;=> T  (animalia package)
(animalia::animalp '#:fish)          ;=> T  (no package)

(animalia::animalp "fish")           ;=> T  (strings, any      
(animalia::animalp "FISH")           ;=> T   case is OK)

If you want quick symbol equality for making animalp faster, you'll have to either

  1. use keywords, which are readily accessible everywhere; or
  2. use symbols defined in the animalia package (and probably export and use them in packages that should have access).

You can also take the approach in id256's answer where you first convert the input to a string, intern it, and then check for membership. However, that must involve the system going through the interning process, which might defeat the purpose, especially for such a small list of animals. It also has the potentially undesirable effect of cluttering up your animalia package. If you're willing to do something like interning, though, it might make more sense to just use a hash table to begin with.

1
votes

The solution with keywords is pretty standard and works fine, but if you want to avoid using it and pass symbol from any package to animalp, you can intern it in the animalia package:

(in-package :animalia)
(defun animalp (thing)
   (and (member (intern (string thing) :animalia)
                '(dog cat snail mouse))
        t))

Note that the second parameter of intern (PACKAGE to intern the symbol in) is optional and by default its value is the *current-package*, which is :cl-user in the moment of call. To deal with this you can explicitly use :animalia.