There has been an interesting discussion going on in squeak-dev about “annotations for service discovery”. Stated simply, the problem is how a third party package can safely extend system-level services in the face of rapid evolution of the underlying system. In Squeak, this mostly affects the following three areas:
- Preferences. Packages often want to show preferences in the standard preferences tools.
- File services. Packages that provide services to operate on files, need to register these.
- Menu items. Packages providing UI elements want to register these with the proper menus.
It sounds simple enough. In practice, these simple things get very messy, very quickly due to the evolution of the system. Here is how it goes: First, you have a new feature and people just use it, say:
MyClass>>initialize
FileList registerFileReader: self.
MyClass>>fileReaderServicesForFile: fullName suffix: suffix
"Return the provided services"
^suffix = 'mysuffix' ifTrue:[
{SimpleServiceEntry
provider: self
label: 'myclass service'
selector: #doServiceWith:
description: 'performs a service'
buttonLabel: 'myclass'}
] ifFalse:[#()]
That’s nice and well, unless you realize that you also want to support older Squeak versions. Ugh. Now we get this:
MyClass>>initialize
(FileList respondsTo: #registerFileReader:)
ifTrue:[FileList registerFileReader: self].
This results in additional Undeclared variable from SimpleServiceEntry. Moving forward, the system keeps evolving, and the services were factored out of FileList and moved to FileServices. Our initializer then becomes
MyClass>>initialize
Smalltalk at: #FileServices ifPresent:[:aClass|
^aClass registerFileReader: self
].
"The legacy variant"
(FileList respondsTo: #registerFileReader:)
ifTrue:[FileList registerFileReader: self].
Etc. I think you can see where this is headed. Every time something changes, we must update our code to play with the latest changes in the underlying system. It is our responsibility to make sure that the code doesn’t break, making it necessary to adapt the code to any changes going on.
The question is: How can we fix that? How can we avoid breaking such extensible services in the first place?
The answer: Method annotations (“pragmas”). They provide an excellent way to discover services provided by a class. Instead of having the class “patch” the system (i.e., via calling methods on various registries directly) the system “discovers” the service provided by a class through the use of annotations.
At this point I need to apologize since I’m shifting gears. The (real) examples above were taken from FileList because that’s where the problems occurred but down below I’ll be using preferences to illustrate how annotations solve the service discovery problem. That’s because we have only implemented them for preferences and so that’s what I’m using here.
To give an example, here is how preferences used to be created and used in Squeak:
Locale class>>initialize
"Locale initialize"
Smalltalk addToStartUpList: Locale.
Preferences addPreference: #useLocale
categories: #('general') default: false
balloonHelp: 'Use the system locale to set the system language etc at startup'.
Locale class>>startUp: resuming
"... snip ..."
(Preferences valueOfFlag: #useLocale)
ifTrue: [
newID := self current determineLocaleID.
newID ~= LocaleID current
ifTrue: [self switchToID: newID]]
Basically, all preference uses were hardcoded to go through Preferences with a very rich (friendly for “bloated”) interface for manipulating preferences. The implementation was then tied into various UI aspects making it virtually impossible to simplify or decouple them.
To fix this, we introduced the annotation #preference:category:description:type: used for example here:
Editor class>>blinkingCursor
<preference: 'Blinking Text Cursor'
category: 'Morphic'
description: 'When true, the text cursor will blink.'
type: #Boolean>
^ BlinkingCursor ifNil: [ true ]
The first thing to notice is that in the client code we’ve put away with an explicit representation for “Preferences”. A preference, as far as a package is concerned is simply a value that you would like to be settable by the user. You can always get and set the value by asking Editor for its #blinkingCursor value – the fact that this value should also be shown as a preference is orthogonal to its existence as a value all in itself.
Secondly, the declaration abstracts away any dependency on the implementation. Any code that deals with preferences and sees this annotation has all the information it needs to present the preference to the user. As a consequence, the only code that is here is the actual domain code – there is no code that deals with the concept of “preferences” in any way. In fact, there may not even be a preference implementation in the system.
Thirdly, using an annotation this way is shifting the burden from the package maintainer to the system maintainer. The package maintainer is no longer required to update their package for each and every Squeak version – it is the task of the system maintainer to implement the service discovery in the proper form.
The scheme described here extends naturally to the other uses mentioned above. In each case we have a “domain action”, be that setting a value (for preferences as in “Editor blinkingCursor”) a method invocation (for a menu, say “Browser open”) or a file operation (for the file list, “SampledSound playWaveFileNamed: …”) that the package provides. In each case we have some additional form in which we’d like to provide that service to the system (via preferences browser, menus, file list operations) and in each case we can announce the availability via an annotation that is discovered by the system.
As a consequence, everyone wins. The package maintainer wins because she has less work to do, deals only with domain code and gets backwards (by backporting the discovery) and implied forwards compatibility. The system maintainer wins because they get the freedom to change core parts of the system without fear of causing undue breakage. A classic win-win situation.