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!
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.
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!
Why do
#+sbcl sbcl-form #+clisp clisp-form #-(or sbcl clisp) not-support-formwhen 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.
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/
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:²
(in-package #:cl-perec) form just
before our defptype and refer to our symbols
package-qualified.
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.
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!