Pro-cl,
I'd like to sanity check some of the lisp idioms my shop has (re)invented.
Some background: We do a lot of web programming, shuffling data to and fro, occasionally doing interesting calculations, but mostly pushing bits around and displaying information in ways humans can use. We have very few CPU bound operations, and most of those are complicated SQL queries, not lisp operations. Most of the applications we create have few users. As such, we haven't put much time into optimizing our lisp code, and rely on dynamism to support rapid development. We have very few type declarations, liberal use of CLOS, not too concerned with consing, etc. Basically using the simplest code we can get away with. It works well for our purposes, and we complicate code for speed when we can't get away with it. We came to lisp from C#, so have a distaste for that style of static typing.
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 ))
The "to" parameter can be anythings that can be mapped to an email address. It is send-email's job to send email, and it will figure it out based on whatever you provide. If it can't do it, it'll tell you. Usually the etypecase is pulled into it's own function, and we have something like:
(defun send-email (to from subject body) (let ((to (coerce-email to))) ;; ... more code ))
And also usually wrapped into a setf-generating macro and we have:
(defun send-email (to from subject body) (coerce-email! to) ;; ... more code )
This is very easy to follow, and when figuring out what to pass for "to", it's trivial to M-. a few times and see what are the allowed values.
In rare cases we want this typecase to be extensible, for example to allow another package to add new mappings. When this happens we end up with a generic method:
(defmethod coerce-email (to) ;;same typecase as before ) ;;elsewhere (defmethod coerce-email ((to server)) (administrator-email to))
Method dispatch takes care of the rest.
Sometimes the coerce-* function is a cond or typecase, and strays outside the flexibility offered by standard method combination, things like using Access's [1] generic interface.
In practice, this is approach is mostly used for converting database CLOS objects to integer IDs and vice versa, depending on whether the function wants an integer ID or an instantiated object.
Some alternatives we tried: * using defmethods, but it didn't seem to offer any advantage over a simple typecase, it was just more typing, more code to read later, and less flexibility if the business wanted something weird. * using check-type or assert to constrain the input options, like C#/Java style static-typing, but that just resulted in duplicate code at call sites to convert from whatever you had to whatever send-email wanted. * having multiple similarly named functions where the function name indicated what type was expected (eg: send-email, send-email-to-client, send-email-to-client-id), this also didn't seem to offer any advantage over a simple typecase * naming parameters after what can be accepted (client-id, email-string, client-or-id, etc) ended up hard to keep up to date.
There are a lot of tradeoffs with this approach, and obviously this wouldn't work for anything with high performance requirements. Has anyone else run into a similar problem and come up with a different/same/better solution? Any problems I'm missing?
[1] https://github.com/AccelerationNet/access
Thanks,