DEFPACKAGE-PLUS is an extensible
DEFPACKAGE variant with predictable cross-platform behavior and some utilities useful for versioning. (See Versioning below.)
DEFPACKAGE+ macro may be used similarly to
DEFPACKAGE. The package
:defpackage+-user-<version> is provided for easily accessing this and other defpackage-plus functionality. For example:
(in-package :defpackage+-user-1) (defpackage+ :my-package (:use #:cl #:alexandria) (:export #:some-symbol))
The following options are available. Each of these are implemented as a call to the corresponding package manipulation function. Repeated evaluations of the
DEFPACKAGE+ form are the same as repeatedly calling each function in sequence, and the behavior of such is stated below:
PACKAGESare inherited as per
USE-PACKAGE. Notably, this is different from
:use, because it only adds packages.
PACKAGESare inherited exclusively; any packages not named are removed as per
UNUSE-PACKAGE. This is like
:export SYMBOLS...: Ensure and export
defpackage, it is not an error or warning for this list to be incomplete.
:export-only SYMBOLS...: Ensure and export
SYMBOLS. Any symbols not named will be unexported.
:export-warning SYMBOLS...: Ensure and export
SYMBOLS. If there are symbols already exported that are not named, issue a warning.
:documentation STRING: Set the package documentation string to
:inherit-from PACKAGE SYMBOLS...: Import
PACKAGE, and also export them.
:inherit PACKAGE: Import all symbols external to
PACKAGE, and also export them.
:inherit-except PACKAGE SYMBOLS...: Import all symbols external to
PACKAGEexcept those named by
SYMBOLS, and export them. (Note that this will not remove symbols previously imported.)
:import-from PACKAGE SYMBOLS...: Import
:import-external PACKAGE SYMBOLS...: Import
:import-from, but it is an error if any symbol in
SYMBOLSis not external to
:import-except PACKAGE SYMBOLS...: Import all symbols from
PACKAGE, except those named by
:import-except-conflicts PACKAGE: Imports everything except symbols that would conflict.
:shadow SYMBOLS...: Ensure and add
SYMBOLSto the shadowing symbols list.
:shadowing-import-from PACKAGE SYMBOLS...: Import
PACKAGE, and add the to the shadowing symbols list.
:nicknames NAMES...: Make
NAMESthe nicknames. Nicknames not named will be removed.
:intern SYMBOLS...: Intern
SYMBOLSif they are not already accessible.
Package definition itself (i.e. the
(defpackage+ NAME) part) happens via
ENSURE-PACKAGE; the package will be created unless one already exists with the given name.
For the most part,
defpackage+ is simply a more convenient
defpackage. You can likely use them interchangeably. However, there are some notable differences:
- Order is significant: Unlike
defpackage, which allows you to specify options in any order, and executes them in a specific order,
defpackage+executes options in the order given.
- Some options such as
:exporthave more permissive behavior depending on your Lisp.
:uselist is by default
NIL. You must specify all packages, such as
- There is no
- There may be other effective differences due to the above.
All options to
defpackage+ are implemented via calls to
defpackage+-dispatch OPTION PARAMS PACKAGE
OPTION is the
car of the option,
PARAMS is the
PACKAGE is the name of the package as specified to
defpackage+. Specialize on
OPTION. Keywords and symbols from
COMMON-LISP are reserved by
defpackage-plus for internal use.
Note, this function does not return an expansion as for a macro. Rather,
defpackage+ itself expands into a series of calls to this function. This makes extensibility much easier.
(defpackage+ :my-package (:use #:cl) (:export #:symbol)) ;; expands to: (eval-when (:compile-toplevel :load-toplevel :execute) (ensure-package ':my-package) (defpackage+-dispatch ':use '(#:cl) ':my-package) (defpackage+-dispatch ':export '(#:symbol) ':my-package))
Package Manipulation Functions
Numerous additional functions are provided in addition to what Common Lisp already provides. These are identical to, and used to implement, the corresponding
ensure-export-warning: These are all functions which ensure the relevant package state (including
ensure-package, which ensures the package named exists).
shadowing-import-from: These provide a "-FROM" form to complement the Common Lisp functions.
import-external-from: The function to complement the
inherit-package-except: These provide import/export functionality.
package-symbols: These return new lists of external symbols and all symbols, respectively, to complement the Common Lisp
There is nothing specific about versions provided per se, but following the stated idiom should provide your users with a stable API as well as the ability to improve the API without conflicts.
A common occurrence: you want to alter your API in some incompatible way. Change how a function works. Alter a default. Not a huge change, but it would be a big improvement. You have a few options:
- Just do it: Break everything else. Your new feature is great, but your users are upset.
- Don't do it: It was a great idea, but too bad... you can't afford to change anything, ever.
- New system: Release an entirely new system, hope people notice. You continue development on the new system, but people are still using the old stuff, so now you're maintaining two... or more.
This is where
defpackage-plus comes in:
(defpackage+ :system-1 ... (:export #:function-1 #:function-2 #:function-3)) (defpackage+ system-2 ... (:inherit-except :system-1 #:function-3) (:export #:function-3))
Simply append an appropriate version number to your package. When you wish to introduce significant (i.e., API-incompatible) changes, simply create a new package with
defpackage+ with a higher version, and use the various
:inherit options to push forward anything that will not change.
This does nothing fancy: it simply imports and exports all external symbols from
FUNCTION-3. At this point, a different
FUNCTION-3 is exported, and it may be defined in whatever new way that's desired.
Both versions may exist simultaneously, even share code, and vary in whatever way the developer needs. Simple, but effective.
(defpackage+ my-package-1 (:use #:cl) (:export function-1 function-2)) (defpackage+ my-package-2 (:use #:cl) (:inherit-from :my-package-1 #:function-1) (:export #:function-3))
In this case
FUNCTION-3 may be an entirely different interface intended to replace
FUNCTION-1 will remain unchanged and available.
Users will simply do the following:
(defpackage+ user-package-1 (:use ... #:my-package-2))
This will ensure they see only the relevant
2 symbols. Of course, users using
defpackage+ is not strictly necessary, but it may be useful in the following examples.
Not all users may, of course, wish to import all of your symbols into their packages, for all the regular reasons. And typing
my-package-2: before every symbol may be a bit onerous to some. The following is also a possibility:
(defpackage+ user-apis-1 ;; Alternatively we could simply (:inherit #:my-package-2 #:some-other-3) (:inherit-from #:my-package-2 symbol-1 ...) (:inherit-from #:some-other-3 another-symbol ...)) (defpackage+ :user-package-1 (:use ... :user-apis-1))
There are many ways to combine this based on preference, of course. If package-local nicknames are available, this may provide the cleanest option, but not all Lisps support this.
What not to do:
Leave off version numbers. It may occur to some people to simply maintain "old" versions with version numbers, and the "latest" version without:
;;; This is a TERRIBLE idea. You just defeated the point: (defpackage+ my-package (:inherit-from :my-package-3 ...)) (defpackage+ my-package-3 (:inherit-from :my-package-2 ...)) ...
This resolves nothing; changing the current package will still break everyone's code, defeating the point. Requiring people to figure out after the fact which version works with their code is even worse. You change things wildly. Much code rot ensues.
Import everything always. It may occur to you to do the following, if you are unfamiliar with how packages and symbols work:
(defpackage my-package-1 ... (:export #:function-1)) (defpackage my-package-2 ... (:inherit my-package-1)) ;;; This is BAD. You just broke 1: (in-package :my-package-2) (defun function-1 (...) ...)
FUNCTION-1 is being redefined while "in"
MY-PACKAGE-2 is irrelevant:
FUNCTION-1 is still imported from
MY-PACKAGE-1, and you are redefining the old function here. What you should have done was
:inherit-except and exported a new version.
- Ryan Pavlik