Resumable exceptions form a key component of the Smalltalk infrastructure. They are one of the standard means of communicating along the call stack, much like Common Lisp’s condition system. They can, however, add a “cross layer” dependency.
Let’s take a look at an example. The Files package provides what you might expect: facilities to write to file, read from files, rename files, and so on. In particular, it has a
. This signals that a file already exists where you were going to save something, and asks that you respond in some manner. It lets “higher level” code decide policy – replace the file, or cancel the operation. So code that wants to always overwrite the file can simply use an exception handler:
[FileDirectory default rename: 'foo.txt' toBe: 'bar.txt']
on: ReplaceExistingFileException do: [:e | e resume: true]
But a standard Squeak image presents a full UI. You’d soon tire of writing the same exception handler to present a standard menu all over the place. Smalltalk provides a facility for this. Implementing
lets you specify a default handler, one that’s only invoked when the exception handling system finds no handlers on the call stack. OK, no problem!
ReplaceExistingFileException >> defaultAction
| selection |
selection := UIManager default
chooseFrom: #('delete version in target directory' 'cancel' )
title: fileName , ' already exists'.
"Smalltalk uses 1-based indexing, so returning 'true' means 'replace it'."
^ selection = 1.
Only now we have a coupling between the Files package and a package in the UI layer (ToolBuilder-Kernel, to be precise). Is there any way we can have our cake (a simple default exception handler) and eat it (not have an “upward” dependency)? (Upward dependencies – low level packages depending on high level packages – not only look bad in a diagram, but make it easy to introduce a cycle in the package dependency graph. When you’re always building up your application state that’s easy enough to avoid. It’s easy to accidentally introduce such a cycle when you build up code state through data migrations.)
As it happens, there is a simple way to avoid the problem. Have your low level package define the exception, and have the higher layer extend the exception and define the default handler. We simply categorise the
in a higher layer package. System-Files looks like a good home.
And there you have it. A resumable exception permits low level code to query high level code, and careful categorising of the methods (monkey patching, in other words) lets you have a simple default handler and still maintain the correct package dependency direction.