pixel is in Charge


17.07.2009   04:49

Announcing PortaCL: The Most Pointless Lisp Library To Date?

  1. .ASD Portability
  2. Extensible Feature Tests
  3. Feature-expression control flow
  4. Give it a Try

While Common Lisp has no shortage of portability libraries to smooth over the differences between implementations, to my knowledge it lacks a wide array of portability library libraries. (The only one I can think of off-hand is trivial-features.)

No more! Now, with PortaCL, portability libraries are a no-brainer!

.ASD Portability

Take this example from trivial-features.asd:

(#+allegro    (:file "tf-allegro")
 #+clisp      (:file "tf-clisp")
 #+cmu        (:file "tf-cmucl")
 #+cormanlisp (:file "tf-cormanlisp")
 #+ecl        (:file "tf-ecl")
 #+lispworks  (:file "tf-lispworks")
 #+openmcl    (:file "tf-openmcl")
 #+sbcl       (:file "tf-sbcl")
 #+scl        (:file "tf-scl")
 #+abcl       (:file "tf-abcl"))
Using portaCL, it becomes:
((:port-file "tf-~/implementation/"))

And adding support for yet-another-obscure-lisp requires nothing more than adding the appropriately-named file.¹

Still not convinced? How about this, from closer-mop.asd:

(:module
 #+allegro "allegro"
 #+clisp "clisp"
 #+ecl "ecl"
 #+lispworks "lispworks"
 #+(or clozure-common-lisp openmcl mcl) "mcl"
 #+(or cmu sbcl) "pcl")
becomes
(:port-module "~/implementation/"
 :alternate-file "pcl")
and
:depends-on (#+allegro "allegro"
             #+clisp "clisp"
             #+ecl "ecl"
             #+lispworks "lispworks"
             #+(or clozure-common-lisp openmcl mcl) "mcl"
             #+(or cmu sbcl) "pcl")
becomes:
depends-on ("~/implementation/")
No muss, no fuss.

Of course, if you've got somewhat lackluster code organization, things get a bit hairier. Take acl-compat.asd, for instance.

Regardless, as you can see, portaCL offers some fairly nice reductions in system definition size for a certain very specific use case. On the other hand, it also increases the magic, making automatic checking of .asd files that much more difficult. But this is Lisp and Lispers hate redundancy.

Extensible Feature Tests

Common Lisp provides and, or, and not feature tests for use in read-time conditionals. I'm fairly certain that's enough, but if it isn't, you can now add your own. For instance, after defining the only-one feature test:

(define-feature-test (only-one :only-one) (&rest rest)
  (= 1 (count-if #'featurep rest)))
you could—assuming you've set up *readtable* properly—, do this:
#-(only-one a b (or c d)) (error "You can't have them all!")

That's right, turing-complete reader-conditionals. And accidental FEXPRs. The insanity! The horror!

Feature-expression control flow

Why do

#+sbcl sbcl-form
#+clisp clisp-form
#-(or sbcl clisp) not-support-form
when you can do
(feature-econd
  (:sbcl sbcl-form)
  (:clisp clisp-form))

Oh, right, because you wanted to use symbols in packages that might not exist in other implementations. Well, so much for that, then.

Give it a Try

Is it useful to you? Is there another style of porting you'd like it to support? Is my code just plain awful? Are you convinced this has to be a joke? Let me know!

For more information, check the project website, notes file, or darcs get http://repo.kepibu.org/portaCL/

  1. And probably pestering me to add support for that lisp to portaCL. Why must there be so many‽
Mood: amused

21.05.2009   14:01

Persisting Simple Types with cl-perec

Previously, Getting Started with cl-perec

You're busy defining your data classes, when without thinking you do something like this:

(deftype scale-1-10 () `(integer 1 10))

(defpclass* pointy-haired-boss (generic-guy)
  ((hair-color :black :type (member :black :blue :green))
   (intelligence 5 :type scale-1-10)
   (moxy 5 :type scale-1-10)
   (grumpiness 5 :type scale-1-10)))

Unfortunately, while :type (integer 1 10) would have worked fine, your named type does not, instead resulting in some unhelpful output:

WARNING: Could not process type SCALE-1-10 specified for slot INTELLIGENCE, falling back to type T. The error was: Unknown type specifier SCALE-1-10
WARNING: Could not process type SCALE-1-10 specified for slot MOXY, falling back to type T. The error was: Unknown type specifier SCALE-1-10
WARNING: Could not process type SCALE-1-10 specified for slot GRUMPINESS, falling back to type T. The error was: Unknown type specifier SCALE-1-10

Not even real conditions, just stuff liable to get lost in miles of scrolling compilation notes.¹ Seemingly, this is easily solved by changing 'deftype' to 'cl-perec:defptype'.

(cl-perec:defptype scale-1-10 () `(integer 1 10))

Sadly, this comes with one rather annoying and unmentioned caveat: your package must :use cl-perec, or at least import the right symbols. You see, defptype uses defclass-star, which when generating accessor methods from slot-names does not use the slot-name's home package, no-no. It uses the current *package*, potentially resulting in undefined methods where there shouldn't be.

The defptype expansion includes this:

(defclass-star:defclass* scale-1-10-type
    (cl-perec:persistent-type)
  ((cl-perec::name 'scale-1-10)
   (cl-perec::args 'nil)
   (cl-perec::body '('(integer 1 10)))
   (cl-perec::substituter …)
   (cl-perec::parser …))
  (:export-accessor-names-p t))

which expands into:

(defclass scale-1-10-type
    (cl-perec:persistent-type)
  ((cl-perec::name … :accessor name-of …)
   (cl-perec::args … :accessor args-of …)
   (cl-perec::body … :accessor body-of …)
   (cl-perec::substituter … :accessor substituter-of …)
   (cl-perec::parser      … :accessor parser-of …)))

Notice how name-of, args-of, body-of, substituter-of, and parser-of are not prefixed by the cl-perec package. That's because they aren't in the cl-perec package, even though they should be, which doesn't really do us much good.

There are a few ways to work around this:²

  1. Import the necessary symbols from cl-perec into our own package.
  2. Create a secondary package which imports all the necessary symbols and exports just the stuff we want in our main package.
  3. Put an (in-package #:cl-perec) form just before our defptype and refer to our symbols package-qualified.
Basically, so long as defptype is macroexpanded in a package that includes symbols from cl-perec, it will generate code that actually does what it's supposed to. Our options are narrowed considerably, however, when we discover upon macroexpansion an assortment of symbols 'helpfully' exported, all related to the internal machinations of defptype. Because we don't want those symbols infesting our primary package, the path of least resistance—and least code—is option number 3: a well-placed in-package.

Not to be deterred from causing trouble, defptype will 'helpfully' try to export your type-name and fail because the symbol it's trying to export isn't available in the cl-perec package, so you'll also need to add an (import '(type-name)) after the in-package.³

To shield ourselves from as many problems as possible, let's package up a combination of options 2 and 3. First, a defstar-shield package.

(defpackage #:defstar-shield
  (:documentation "Beware proton torpedoes.")
  (:use #:cl #:cl-perec)
  (:shadowing-import-from #:cl-perec #:set #:time)
  (:shadow #:defptype)
  (:export #:defptype))

Then a wrapper around cl-perec:defptype.

(in-package #:defstar-shield)
(defmacro defptype (name args &body body)
  "Wrapper around cl-perec:defptype."
  (let ((package (package-name *package*)))
    `(eval-when (:compile-toplevel :load-toplevel :execute)
       (in-package #:defstar-shield)
       (import '(,name)) ; lame
       (cl-perec:defptype ,name ,args ,@body)
       (in-package ,package))))

This is a bit messy, but fairly simple: it arranges *package* so cl-perec:defptype gets macroexpanded within the defstar-shield package, making sure not to leak that package change into your surrounding code.

That finally out of the way, you continue on.

(defun string-has-even-length-p (str)
  (evenp (length str)))

(cl-perec:defpclass* tps-report ()
  ((report-title nil
     :type (and string
                (satisfies string-has-even-length-p)))))

And once again, you are met with frustrating failure. cl-perec doesn't really support (and) type specifiers, and so when you try to create a tps-report you end up with errors thorough investigation reveals to be related to the string type. You see, cl-perec's type mapping is a bit awkward. For instance, the string mapping machinery gets called for an '(and string …) type, and then promptly mistakes (and …) for (string n), and tries to determine the string's length, which goes over about as well as you might expect.

At this point, we've got a couple of options: we can dig into the string mapping and fix that, or we can give our type a name and trick cl-perec into thinking it's a primitive type. I've tried to go both ways, and the latter is less work (and more in line with using a library), so that's what I'll show you.

First, we give our type a name.

(defstar-shield:defptype even-string ()
  `(and string (satisfies string-has-even-length-p)))

Then we push it onto the list of canonical types.

(pushnew 'even-string cl-perec::*canonical-types*)

This ensures cl-perec won't convert 'even-string into '(and string …), which was causing our woes. At this point, cl-perec will treat even-strings as though they were regular strings—quite possibly by accident, but it works.

(cl-perec:defpclass* tps-report ()
  ((report-title nil :type even-string)
   (report-text nil :type string)))

And now we can generate tps-reports, with the strange and arbitrary requirement that their titles have an even number of characters.

Later, our boss comes by and says each TPS report should be barcoded for easy automatic identification.

(defun calculate-checkdigit (barcode)
  "UPC-A barcode checkdigit."
  (- (nth-value
      1
      (ceiling
       (loop :for digit :across barcode
             :for pos :from 1
             :for num = (digit-char-p digit)
             :sum (if (oddp pos)
                      (* 3 num)
                      num))
       10))))

(defun valid-barcode-p (barcode)
  "Returns true if the given string is a valid barcode."
  (and (= 12 (length barcode))
       (every #'digit-char-p barcode)
       (= (calculate-checkdigit (subseq barcode 0 11))
          (digit-char-p (char barcode (1- (length barcode)))))))

(defstar-shield:defptype barcode ()
  `(and (string 12) (satisfies valid-barcode-p)))
(pushnew 'barcode cl-perec::*canonical-types*)

(cl-perec:defpclass* tps-report ()
  ((barcode nil :type barcode)
   (report-title nil :type even-string)
   (report-text nil :type string)))

You get all that coded up only to discover your barcode type isn't creating a fixed-length field in the database like it should. All our fancy workarounds are coming back to haunt us!

We could backtrack, figure out how to fix the combination of (and string) types in cl-perec, and go from there. Or, we could trudge along delving further into the scary guts of cl-perec.

Delving farther in than a library user should probably go, we discover cl-perec::defmapping, which looks promising. Copying things from the default string defmapping and modifying slightly, we come up with something that works.

(cl-perec::defmapping barcode
  (cl-rdbms::sql-character-type :size 12)
  'cl-perec::identity-reader
  'cl-perec::identity-writer)

It's pretty scary that everything we use is unexported, but what we're doing is pretty simple: this says the lisp-type 'barcode equates to an SQL string 12 characters long. identity-reader and identity-writer are the functions that convert a string from SQL into a lisp object—and vice versa—in this case, by doing nothing.

Regrettably, this all feels pretty hacky. After all, we've just gone to a lot of trouble for no other reason than that cl-perec doesn't properly support (and string satisfies) types. But we've got an application to write, so it'll have to do for now.

Footnotes

  1. a comment inside cl-perec notes due to the MOP we must not fail when this is called, otherwise the entire (sblc) [sic] image breaks. So much for using the debugger!
  2. I don't include fixing defstar-class in this list. For all I know, it does this stuff intentionally.
  3. This means your type name cannot conflict with any symbol in your defstar-shield package. If this might be a problem, be wary of which packages are used by your defstar-shield package, possibly limiting yourself to importing only necessary symbols.
  4. It also breaks the return value, but you didn't want that anyway.
  5. Common Lisp lacks any standardized facility to perform type inspection, so we can't really blame cl-perec for the defptype requirement.
  6. Common Lisp type specifiers are turing-complete, which makes full automatic conversion to database types impossible. (and string) may not seem difficult, but it isn't far to a fairly hairy type specifier.