Skip to content

Instantly share code, notes, and snippets.

@phoe
Last active December 1, 2024 06:58
Show Gist options
  • Save phoe/2b63f33a2a4727a437403eceb7a6b4a3 to your computer and use it in GitHub Desktop.
Save phoe/2b63f33a2a4727a437403eceb7a6b4a3 to your computer and use it in GitHub Desktop.
Package-local nicknames in Common Lisp - a semishitpost about PLNs

Package-local nicknames in Common Lisp

Warning: this is a rant.

Warning: you have been warned.

Note: actually worthwhile content starts in the second subsection. You are free to skip the first one.

Story time

Since times unknown, Common Lispers have had issues with package name collisions. It was thought that nicknames would, perhaps, be a solution, but nicknames are just as global as standard names so they didn't really help much. The programmer could use RENAME-PACKAGE, but that was just as painful since it affected the global namespace as well. This made people angry and frustrated, especially when, one day, Xach couldn't load all of Quicklisp into one Lisp image because of petty name conflicts, among other things. Perhaps this will be the solution.

To describe the problem in more detail:

cl-opengl wants the nickname GL:. CLX/GLX wants the nickname GL:. A hypothetical Parenscript/WebGL wants the nickname GL:. It is reasonable to want to load more than one of these libraries at once, and to want to use the nickname GL: for more than one of them from different code, without having to remember to RENAME-PACKAGE a bunch of packages before loading new code (particularly when doing interactive development on more than one of them at once). Specifying nicknames within the creating package leads to conflicts like these, particularly since there may be multiple 'obvious' nicknames to define. This occurs even if nobody ends up using more than one of them for a given package.

So, if package nicknames were global, then someone thought that it would be beneficial to have some sorta nicknames that weren't global. So, they were local. Local to what? Perhaps to some unit of Lisp code that could depend on other packages on its own. If we only had such a structure at our disposal...

Oh! A package was such a structure!

So, the idea of package-local nicknames was born. I have no idea when, but, around 2013 (or so), a certain someone called Nikodemus implemented that idea in SBCL. From there, PLNs trickled into other implementations.

Right now, at the end of 2019, it seems that SBCL, CCL, ECL, Clasp, ABCL, and ACL have it implemented; LispWorks is preparing a release with the implementation; and CLISP is on my personal target in order to sate my OCD and to have PLNs available across all of za contemporary Lisp warudo(1).

What does it mean? In my package, a package named FOOBARBAZ-QUUX.FOO can be referred to as F, and I can make use of a symbol that I call F:FROBNICATE. In another person's package, this package could be referred to as FOO, and the same symbol is called FOO:FROBNICATE. Yet another person can refer to that package as UTILS and therefore refer to that symbol as UTILS:FROBNICATE. All of these worlds do not collide with each other, which means that package authors no longer need to try to cater to the users' possible preferences while - at the same time - biting each other's conses to try and hog the best nicknames for themselves. Nope. Nicknames that collide with each other can be long gone now: package-local nicknames do not have this shortcoming of being global.

Because, come on. Maybe I want to call your package Fred. And I want this nickname to stay between me and your package. I'mma call it that only when we're alone. I don't feel comfortable with exclaiming, "hey, everyone, please call this package Fred when you refer to it!". Only me. When I refer to it from inside my code. There are no other Freds in my life codebase. Or, uhhh, at least in this package of mine.

And no one else should need to ever care about that.

Actual worthwhile content starts here

Package-local nicknames work just like standard nicknames, except they do not work globally. The system takes the current value of the *PACKAGE* dynamic variable while performing package lookup; if the current package has any local nicknames, then they are taken into account when performing the package search, and the local nickname of a package resolves to the package that the local nickname is bound to.

This means, among other things, that:

  • FIND-PACKAGE will use package-local nicknames when searching for package objects,
  • INTERN will use package-local nicknames to find package to intern into,
  • the Lisp reader will use package-local nicknames when reading Lisp symbols.

A practical example is the following:

(ql:quickload :alexandria)

(defpackage my-package 
  (:use #:cl)
  (:local-nicknames (#:a #:alexandria.dev.0))) ;; (2)
  
(in-package :my-package)

(defun assoc-value-or-die (alist key &key (test 'eql))
  (multiple-value-bind (value foundp) (a:assoc-value alist key :test test)
    (if foundp
        value
        (error "Value ~S not found in alist ~S under test ~S."
               key alist test))))

Note the use of (a:assoc-value alist key :test test). This will also work if we use RENAME-PACKAGE to add a global nickname A to package ALEXANDRIA.DEV.0, but...

(in-package #:cl-user)

(find-package :a)

The above will likely return NIL, since we are no longer in package MY-PACKAGE, where the local nickname was in effect.

What does it mean? It means that we can define another package, that nicknames another package as A:

(ql:quickload :golden-utils)

(defpackage my-other-package 
  (:use #:cl)
  (:local-nicknames (#:a #:golden-utils))) ;; (3)
  
(in-package :my-other-package)

(defun assoc-value-or-die (alist key &key (test 'eql))
  (multiple-value-bind (value foundp) (a:alist-get alist key :test test)
    (if foundp
        value
        (error "Value ~S not found in alist ~S under test ~S."
               key alist test))))

Again, note the use of (a:alist-get alist key :test test).

Alexandria does not have any symbol with name ALIST-GET, and the Golden Utils do not have any symbol named ASSOC-VALUE. But, even if they had, there is no collision here - package-local nicknames ensure that the nickname A resolves to completely different package objects while we stay in MY-PACKAGE and MY-OTHER-PACKAGE.

That's all that you usually need to know, really. The rest is just some API for programmatic manipulation of PLNs:

  • package-local-nicknames returns the list of local nicknames of a package,
  • package-locally-nicknamed-by-list returns the list of all packages that locally nickname a package,
  • add-package-local-nickname and remove-package-local-nickname exist because the API for adding and removing global nicknames (yes, I mean you, RENAME-PACKAGE) sucks.

<shameless-plug>Grab a portability library if you want to use these across implementations. (You likely won't need one, though, since the most important part is already included in defpackage and therefore available out of the box, sooooo~)</shameless-plug>


So, that's it! Go, write Lisp, enjoy the soon-to-be-fully-portable language extension, and may your packages never collide again in the code that you write.(4)

Footnotes

  1. TIME IS ALWAYS STOPPED IN THE LISP WORLD ゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴゴ
  2. Yes, ALEXANDRIA is not the proper name of the Alexandria package! It is merely a nickname.
  3. It makes sense, because "A" is for "aurum", latin for "gold". Trust me.
  4. Yes, this is actually a good reason for pjb to legitimize the fully qualified package names he'd been using for, what, maybe decades now. His package name of COM.INFORMATIMAGO.COMMON-LISP.HEAP.MEMORY is pretty much not guaranteed to collide with anything, at least when compared to ALEXANDRIA or GOLDEN-UTILS, and now it is finally possible to utilize that package (and all of pjb's software!) in a sane way without USEing it(5) - (:local-nicknames (#:memory #:com.informatimago.common-lisp.heap.memory)) and tah-daah! It's working. God damn, pjb was right, he was right the whole damn time - it was just the Lisp world that wasn't ready for that truth.
  5. The :USE option of :DEFPACKAGE is, as of now, deprecated. Seriously. (declaim (deprecated :use)). Don't use USE. Only use USE if you want to use the CL package, or some equivalent of it for when you work with Qtools which has its own CL+QT package. USE is a bad idea in contemporary code except for internal packages that you fully control, where it is a decent idea until you forget that you mutate the symbol of some other package while making that brand new shiny DEFUN. USE is the reason why Alexandria cannot nowadays even add a new symbol to itself, because it might cause name collisions with other packages that already have a symbol with the same name from some external source. It's good when it is USEd from another package, but if Alexandria wanted to export a function named FOO, and, in your package that USEs Alexandria you have a DEFUN FOO, I bet $5 that you imagine the havoc that this might break all over your Lisp image and all the code that has decided to jump ships and depend on ALEXANDRIA:FOO that you just hopelessly mutated while ignoring the ASDF warnings that came from compiling the file because Quicklisp doesn't show warnings by default while loading shit and you, just doin' business as usual, used QL:QUICKLOAD instead of ASDF:LOAD-SYSTEM and didn't even tell Quicklisp to be :VERBOSE while loading. Siiiiiiiigh...

Buuuut, that's a rant for another day.

@Ecsodikas
Copy link

Thanks for this write up. It was a very good read!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment