JavaScript 2.0
Rationale
Versioning
|
Tuesday, October 9, 2001
As a package evolves over time it often becomes necessary to change its exported interface. Most of these changes involve adding definitions (top-level or class members), although occasionally a definition may be deleted or renamed. In a monolithic environment where all JavaScript source code comes preassembled from the same source, this is not a problem. On the other hand, if packages are dynamically linked from several sources then versioning problems are likely to arise.
One of the most common avoidable problems is collision of definitions. Unless we solve this problem, an author of a library will not be able to add even one definition in a future version of his library because that definition’s name could already be in use by some client or some other library that a client also links with. This problem occurs both in the global scope and in the scopes of classes from which clients are allowed to inherit.
Here’s an example of how such a collision can arise. Suppose that a library provider creates a library called BitTracker
that exports a class Data
. This library becomes so successful that it is bundled with all web browsers produced
by the BrowsersRUs company:
package BitTracker; public class Data { public field author; public field contents; function save() {...} }; function store(d) { ... storeOnFastDisk(d); }
Now someone else writes a web page W that takes advantage of BitTracker
. The class Picture
derives from Data
and adds, among other things, a method called size
that returns the dimensions
of the picture:
import BitTracker; class Picture extends Data { public method size() {...} field palette; }; function orientation(d) { if (d.size().h >= d.size().v) return "Landscape"; else return "Portrait"; }
The author of the BitTracker
library, who hasn’t seen W, decides in response to customer requests
to add a method called size
that returns the number of bytes of data in a Data
object. He then releases
the new and improved BitTracker
library. BrowsersRUs includes this library with its latest NavigatorForInternetComputing
17.0 browser:
package BitTracker; public class Data { public field author; public field contents; public method size() {...} function save() {...} }; function store(d) { ... if (d.size() > limit) storeOnSlowDisk(d); else storeOnFastDisk(d); }
An unsuspecting user U upgrades his old BrowsersRUs browser to the latest NavigatorForInternetComputing 17.0
browser and a week later is dismayed to find that page W doesn’t work anymore. U’s granddaughter Alyssa
P. Hacker tries to explain to U that he’s experiencing a name conflict on the size
methods, but U
has no idea what she is talking about. U attempts to contact the author of W, but she has moved on to
other pursuits and is on a self-discovery mission to sub-Saharan Africa. Now U is steaming at BrowsersRUs, which
in turn is pointing its finger at the author of BitTracker
.
How could the author of BitTracker
have avoided this problem? Simply choosing a name other than size
wouldn’t work, because there could be some other page W2 that conflicts with the new name. There are several possible
approaches:
com_netscape_length
method while MIT’s objects used the edu_mit_length
method.The last approach appears to be the most desirable because it places the smallest burden on casual users of the language, who merely have to import the packages they use and supply the current version numbers in the import statements. A package author has to be careful not to disturb the set of visible prior-version definitions when releasing an updated package, but authors of dynamically linkable packages are assumed to be more sophisticated users of the language and could be supplied with tools to automatically check updated packages’ consistency.
JavaScript 2.0 employs namespaces to provide safe versioning. A package can export several namespaces, each of which provides a different view of the package’s contents. Each namespace corresponds to a version of the package’s API.
A version describes the API of a package. A release refers to the entirety of a package, including its code. One release can export many versions of its API. A package developer should make sure that multiple releases of a package that export version V export exactly the same set of definitions in version V.
As an example, suppose that a developer wrote a sorting package Sorter
with functions sort
and merge
that called bubble sort in the initial version. In the next release the developer adds a function called stablesort
and includes it in version V2
. In a subsequent release V3
, the developer changes the sort
algorithm to a quicksort that calls stablesort
as a subroutine and adds the permute
function. That
last release of the package might look like:
package Sorter { explicit namespace V2; explicit namespace V3; internal const V2and3 = V2 V3; public var serialNumber; public function sort(compare: Function, array: Array):Array {...} public function merge(compare: Function, array1: Array, array2: Array):Array {...} V2and3 function stablesort(compare: Function, array: Array):Array {...} V3 function permute(array: Array):Array {...} }
Suppose, further, that client packages C1 and C2
both import Sorter
. There is only one instance of Sorter
running — the latest release.
By default, both C1 and C2 see only Sorter
’s
original API. However, suppose that C2 is aware that Sorter
has been
extended and would like to also use some of its newer functionality. To do this, C2
evaluates the magic incantation
use namespace(Sorter.V2);
after it imports Sorter
. This enables C2 to also see the stablesort
function. Note that, in this example, both clients see the same sort
and merge
functions and the
same serialNumber
variable (in particular, if C1 wrote to serialNumber
, then C2
would see the updated value), but only C2 can see the stablesort
function. Both clients get the quicksort
release of sort
. If client package C1 defined its own stablesort
function, then that
function would not conflict with Sorter
’s stablesort
; furthermore, Sorter
’s sort
would still refer to Sorter
’s stablesort
in its internal subroutine call.
Had only the first release of Sorter
been available, client C2 would
obtain an error because Sorter.V2
would be undefined. Client C1 could
run normally, although the sort
function it calls would use bubble sort instead of the quicksort.
The example above illustrates versioning as it applies to a package’s globals. The same techniques can be used to add members to existing classes, and there versioning is much more useful.
Waldemar Horwat Last modified Tuesday, October 9, 2001 |