Finally!
A while ago, I'd written about raising and handling exceptions in muSE. The key idea, borrowed from Common Lisp, is that when an exception is raised, the execution context should not be unwound to the point where the exception can be handled. This allows an exception handler to resume execution from the point the exception occurred or try another handler, starting again from the inner most context. The current try-raise-retry facility in muSE manages to capture this requirement fairly well. However, there was one glaring omission - the inability to specify scope limited cleanup operations. In v332, muSE gets a cleanup mechanism ... finally ... I mean literally a finally block .. and this post is all about it. If you're going "oh yeah I know all about finally, Java has it", I humbly urge you to read on. You'll wish you had this in Java.
In muSE, you can now place a code in a finally block that can feature anywhere within an expression protected by a try. "Anywhere" doesn't just mean the visible lexical scope, but the entire execution scope. Not only can you have finally anywhere, you can have as many of it as you need - you can even accumulate finalizing actions in a loop. The finally block gets captured into a thunk that gets evaluated at the end of the scope of the try context in which the block occurs. The thunks execute in the reverse order in which they are created. The thunks can refer to any variable in its lexical context, since a closure is created when evaluating a finally block.
Here is a simple example that reads a number from file F, multiples it by a given value and returns the result.
; Raises an exception if the file couldn't be opened.
(define (safe-open file)
(let ((f (open-file file 'for-reading)))
(if (eof? f)
(raise 'FileOpenError file)
(do (finally (print "Closing file" file) (close f))
f))))
; Read a number from a file
(define (read-number file)
(define F (safe-open file))
(let ((n (read F)))
(if (number? n) n (raise 'NotANumber n))))
Here is a way to read a number from "F.scm" and if you don't get a number, use a default value -
(try (read-number "F.scm")
(fn (ex 'NotANumber n)
42))
Here is a way to read a number from "F.scm" and if the file doesn't exist, use "Default.scm" instead -
(try (read-number "F.scm")
(fn (cont 'FileOpenError file)
(cont (safe-open "Default.scm"))))
When do exceptional conditions occur?
In pure computations, they can happen when an unsupported - "unexpected" - input value is encountered when calculating the result. Let us call these p-type exceptions. The other type is when dealing with side-effecting computations, of which IO is arguably the most dominant case. Let us call these s-type exceptions.
p-type exceptions can be dealt with in a language which allows one to substitute another expression for one containing the exceptional case, when the case does happen. muSE does this fairly well with its try-raise-retry operators. The example function in the earlier article shows how a pure function with an exceptional condition can be tamed by the calling context, given an expression substitution mechanism.
s-type exceptions cannot be dealt with adequately with a pure-expression substitution mechanism. In the interest of composability, it is often desirable to wrap side-effecting expressions into what feels like a pure function - for example, a data structure lookup that appears to never fail might be implemented via a connection to a remote database. To do that, we need to be able to execute one or more side-effecting cleanup operation, such as closing the database connection, before the wrapper function returns to its calling context.
With the new finally block, I believes muSE satisfies both these cases.