[pro] style question about types, coercion, expectations for function parameters

Ryan Davis ryan at acceleration.net
Thu May 31 18:50:36 UTC 2012


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,

-- 
Ryan Davis
Acceleration.net
Director of Programming Services
2831 NW 41st street, suite B
Gainesville, FL 32606

Office: 352-335-6500 x 124
Fax: 352-335-6506





More information about the pro mailing list