In article 4FC7BD7C.7050205@acceleration.net, Ryan Davis ryan@acceleration.net wrote:
I'd like some more opinions on a pattern that has cropped up. One of the problems we were having was quickly determining what a function expected for it's arguments. As a somewhat contrived example, SLIME helpfully would tell me that #'send-email wanted (to from subject body), but then it was left to me to guess what values I should pass in. In real code this was frequently non-trivial, and we'd be hand-tracing to figure out where the parameter was used to figure out what it should be. Should "to" be a string, a CLOS Client object, or the database ID of a Client? The answer we arrived at was "yes":
(defun send-email (to from subject body) (let ((to (etypecase to (string to) ((integer 0) (email (fetch-client to))) (client (email to)) ))) ;; ... more code ))
Hi Ryan!
As others have pointed out the use of designators is widely used both in the Common Lisp language itself, and in real world usage.
Here's my experience which matches yours:
* As Stelian has pointed out, lifting the ETYPECASE into a separate function e.g. EMAIL or EMAIL-DESIGNATOR is usually a good idea.
* If user of the code can meaningfully extend the notion of what an email is, then it's a good idea to also export a generic function COERCE-EMAIL-DESIGNATOR which is supposed to take a user's object and turn it into something that the above function EMAIL-DESIGNATOR can understand.
If CLIENT is a class, users can already subclass that one for extension. However:
Often you have a thing that simply contains a CLIENT (e.g. a SESSION), and you might want to be able to just pass the SESSION object to send-email to stand for its CLIENT slot. In that case COERCE-EMAIL-DESIGNATOR would be the right thing.
* You may also want to define a type EMAIL in your case, and declaim the ftype of your functions to take those.
Or use something like DEFINE-API which does exactly that in a more succinct way. You can find it in named-readtables, or sequence-iterators.
E.g. in your example:
(deftype email () `(or string unsigned-byte client))
(define-api send-email (to from subject body) (email email string string => (values R1 R2)) ; R1,R2 (let ((to (email-designator to)) ; return (from (email-designator from))) ; types ...)
This has two advantages: 1. C-c C-d d will show you the function's argument types (unfortunately fully expanded in case of SBCL. It would be nice if it stored the original types and gave access to it.) 2. passing a wrong type can be caught at compile time if that type is known at compile time.
E.g. writing (send-email :A :B ...) will result in a warning at compile-time.
3. Combined with good docstrings, you can produce nice documentation of the code automatically. The Documentation for named-readtables and sequence-iterators were entirely auto-generated. Combined with hyperdoc and slime-hyperdoc, you can get access to documentation and argument types directly from within Slime very conveniently.
T