;; Copyright (c) 2011 nklein software ;; MIT License. See included LICENSE.txt file for licensing details.
Patrick Stein pat@nklein.com
When I wrote this library, I had read about ContextL and decided that I only wanted a very small subset of it and wanted it to be very easy to use. It turns out that I didn't quite understand the complexity-level of ContextL. It is actually very similar to what I have described below.
With this library, you set up some versions and a variable to track the current version:
(method-versions:define-method-version :v1.0)
(method-versions:define-method-version :v1.1 :v1.0)
(declaim (special *protocol-version*))
(defparameter *protocol-version* :v1.0)
In ContextL, you just set up some layers:
(contextl:deflayer :v1.0)
(contextl:deflayer :v1.1 (:v1.0))
In my library, you then set up a generic function that uses a special method combination that keys off of the special variable:
(defgeneric send-cmd (cmd)
(:method-combination method-versions:method-versions-method-combination
*protocol-version*))
In ContextL, you declare a layered function:
(contextl:define-layered-function (cmd))
In my library, you then declare different methods using the version as a method qualifier.
(defmethod send-cmd ((cmd login-cmd))
(send-string (login-name cmd))
(send-string (login-password cmd)))
(defmethod send-cmd :v1.0 ((cmd login-cmd))
(send-string (login-name cmd))
(send-string (login-password cmd))
(send-string (login-location cmd)))
In ContextL, you declare layered methods specifying which layer the functions belong to:
(contextl:define-layered-method send-cmd ((cmd login-cmd))
(send-string (login-name cmd))
(send-string (login-password cmd)))
(contextl:define-layered-method send-cmd :in :v1.0 ((cmd login-cmd))
(send-string (login-name cmd))
(send-string (login-password cmd))
(send-string (login-location cmd)))
In my library, you set your special variable appropriately and invoke the method:
(let ((*protocol-version* :v1.1))
(send-cmd cmd))
In ContextL, you declare which layer you want to be active when you go to invoke the method:
(contextl:with-active-layers (:v1.1)
(send-cmd cmd))
The library below does not let you specify other method qualifiers
like :around
or :after
. ContextL does.
I am going to leave this library published because I think it is a reasonably understandable, yet non-trivial, use of non-standard method combinations. However, I am going to end up using ContextL for the projects that I had intended for this library.
There are situations where one might like to dispatch a method on some information other than the required parameters of the method. For many situations, it is sufficient to switch between those methods based on some external parameter. The method-versions library allows one to do just that.
For more complicated situations where one also wants to turn on and off slots within classes, one might prefer ContextL. For situations where one wants to dispatch a method based on predicates on the arguments, one might prefer Filtered Functions.
- The home page: http://nklein.com/software/method-versions/
- The tar-ball: method-versions-0.1.2011.05.18.tar.gz
- The GPG signature for the tar-ball: method-versions-0.1.2011.05.18.tar.gz.asc
- The main git repository: http://git.nklein.com/lisp/libs/method-versions.git
- A browsable mirror of the git repository: http://github.com/nklein/method-versions
The ContextL library provides:
- user-defined layers
- methods defined differently in different layers
- functions defined differently in different layers
- slots only available when certain layers are active
- reflective layer activation allowing one to:
- make layer A depend on layer B
- make layer B exclusive with layer C
- etc.
For the serialization, logging, and network protocol requirements that I expect to need in some of my other projects, ContextL seemed like overkill. This library does a very small portion of that. Its only goal is to allow a hierarchy of method definitions and selecting a particular point in the hierarchy.
The method-versions library provides a method-combination one can use to create multiple versions of a method and switch which one is invoked through a special variable.
For example, suppose you were working on a protocol for a simple client-server application. You may have methods like this:
(defmethod send-cmd ((cmd login-cmd))
(send-string (login-name cmd))
(send-string (login-password cmd)))
(defmethod recv-cmd ((cmd login-cmd))
(setf (login-name cmd) (recv-string))
(setf (login-password cmd) (recv-string)))
After a few months of beta-testing, you want to extend the login
command to include a geographic location. If you just update your
send-cmd
and recv-cmd
methods for the login-cmd
, then you will
have to jump through all kinds of hoops to make sure that you can
handle receiving a login command from users with the new send-cmd
in
their binary and users with the old send-cmd
in their binary.
You can use the method-versions library to create a version of your server that can handle both versions in a sane manner.
First, you define a version for the new variant of your protocol.
(method-versions:define-method-version :v1.0)
Note: The define-method-version
accepts an optional second parameter
which specifies the parent version. So, for future expansion, one could
end up with the following:
(method-versions:define-method-version :v1.0)
(method-versions:define-method-version :v1.1 v1.0)
(method-versions:define-method-version :v1.2 v1.1)
(method-versions:define-method-version :v2.0 v1.2)
(method-versions:define-method-version :v1.1-bugfix-1 v1.1)
After you have established your versions, you define a special
variable which will track which version of your methods you would like
to use during any given invocation and declare your generic method to
use the method-versions-method-combination
with your tracking
variable.
(declaim (special *protocol-version*))
(defparameter *protocol-version* :v1.0)
(defgeneric send-cmd (cmd)
(:method-combination method-versions:method-versions-method-combination
*protocol-version*))
(defgeneric recv-cmd (cmd)
(:method-combination method-versions:method-versions-method-combination
*protocol-version*))
Next, you declare your default versions as before and declare your
:v1.0
versions using the version as a method qualifier. (For brevity,
we only show the recv-cmd
here, but the send-cmd
is similar.)
(defmethod recv-cmd ((cmd login-cmd))
(setf (login-name cmd) (recv-string))
(setf (login-password cmd) (recv-string)))
(defmethod recv-cmd :v1.0 ((cmd login-cmd))
(setf (login-name cmd) (recv-string))
(setf (login-password cmd) (recv-string))
(setf (login-location cmd) (recv-string)))
Now, if you want to receive a message coming in from a client who is
using the original version, you would ensure *protocol-version*
is set to nil
and then invoke the recv-cmd
method.
(let ((*protocol-version* nil))
(recv-cmd cmd))
For clients who are using the new version, you would set the
*protocol-version*
appropriately and then invoke the recv-cmd
method.
(let ((*protocol-version* :v1.0))
(recv-cmd cmd))
Of course, you will have to know which version of the protocol the client is sending. We recommend that you assume the client is using the default version and add a message to your connection establishment phase where the client tells the server what version the client is using.
(defmethod send-cmd ((cmd version-cmd))
(send-string (package-name (symbol-package *protocol-version*)))
(send-string (symbol-name *protocol-version*)))
Then, use that version when receiving subsequent commands from that client.
In this example, we do a silly form of internationalization. To that end, we will use English as the default language and define some other languages.
(method-versions:define-method-version latin)
(method-versions:define-method-version pig-latin)
(method-versions:define-method-version french latin)
(method-versions:define-method-version spanish latin)
We will prepare a language parameter and a welcome method that is versioned on the language.
(declaim (special *language*))
(defparameter *language* nil)
(defgeneric welcome ()
(:method-combination method-versions:method-version-method-combination
*language*))
And, we define welcome methods for the various languages (accidentally
forgetting spanish
).
(defmethod welcome () :welcome)
(defmethod welcome :latin () :velkominum)
(defmethod welcome :pig-latin () :elcomeway)
(defmethod welcome :french () :bonjour)
Then, we will try each of the languages in turn.
(mapcar #'(lambda (ll)
(let ((*language* ll))
(welcome)))
'(nil :latin :pig-latin :french :spanish))
=> (:welcome :velkominum :elcomeway :bonjour :velkominum)