Scheme Load Return Values
Shortly after finishing my previous post, I began to wonder if maybe some of the features I had requested were available already. I was pleasantly surprised.
Implementations tested were:
Bad news first. In my hour or so of testing, I was unable to find any way to directly return values from a loaded file in Guile, Gambit, and STklos.
On the other hand, that means that four of the seven implementations that I tested could return values.
MzScheme and MIT Scheme both return the last form, whatever it was. If the last form was a DEFINE statement, MzScheme returns nothing, and MIT Scheme returns the identifier that was defined.
Bigloo doesn't seem to return anything usable, but it does print the results of intermediate evaluations, and claims to return the result of the last evaluation. This may indeed be true, but in my testing I was unable to get it to return anything other than the path of the just-loaded file.
Chicken, however, was an unqualified success. Although it is true that at first it returns nothing from its LOAD function, it provides a brilliant little mechanism to fix that. The LOAD function, in addition to taking a filename, will also take an optional argument called EVALPROC, which is called repeatedly with each form from the loaded file. This fact, combined with a little closure magic, ends us up with this:
(let ((return '()))
(load file
(lambda (exp)
(if (and (list? exp) (eqv? (list-ref exp 0) 'define))
(begin
(eval exp)
(set! return (cons (list-ref exp 1) return)))
(set! return (cons (eval exp) return)))))
(reverse return)))
This function calls the LOAD function, but passes it its own evaluation procedure, which will assemble a list of values when called, as well as evaluating each expression.
After seeing the beautiful and elegant way in which Chicken Scheme allows the user to dynamically alter the behaviour of the LOAD function at runtime, I feel like I should alter my recommendation for modifying that behaviour. Chicken has, I believe, found the best possible way to implement such functionality: have a reasonable default, but allow people to override it if they wish. Simple, elegant, and powerful: exactly like the language itself. Bravo, Chicken.
Bravo.













January 16th, 2009 - 10:01
A more portable way (should work in all implementations) to achieve this is this:
(call/cc (lambda (k)
(parameterize ((*return* k))
(load "file")))
And in the file you load, you can just say
I do hope this will come out alright. (wordpress has no “preview comment” mode?)
January 16th, 2009 - 17:40
Correct me if I’m wrong, but doesn’t that at least require SRFI-39?
While I admire the elegance of that solution, the only Scheme implementations listed* as supporting that one are PLT, Kawa, Guile, Chicken, and SISC. Of these, Guile does not have it enabled by default, PLT and Chicken do, and the other two are not installed on my system so I can’t test their claims.
Besides which, and I may well be wrong on this because I can never quite understand continuations, wouldn’t that second example have to be the last form in the loaded file? And on most implementations it would then become impossible to load the file without using that function, because *RETURN* would be unbound.
I do like that solution for a very narrowly defined problem set, but I think that a more general solution might be better for this problem.
* At http://srfi.schemers.org/srfi-implementers.html
January 17th, 2009 - 08:20
You’re right that this particular implementation requires SRFI-39. It’s not required for the idea, though: you could also set! a global variable with the value of that continuation, but than you’d have to build in some save/restore mechanism for handling nested LOAD calls (which in practice comes down to an ad-hoc implementation of SRFI-39). By the way, that list of supported systems does not seem very up-to-date. I mean, Chicken 1.0?
*RETURN* doesn’t need to be the last form of the loaded file. If it’s a form halfway down the file, execution is halted and control is passed back to the calling file. But it’s the last form that’s evaluated in the file, indeed. This would be behaviour I’d expect; its behaviour is the same as any other (implicit or explicit) BEGIN. Result of the last form is returned.
Loading the file with *RETURN* unbound isn’t a problem if you stick this mechanism into a separate library, which you must load in every file that uses *RETURN* (which you would do with any other procedure or variable you want to introduce).
The parameter cannot be initialized as #f in this case, but would need to be initialized to EXIT or to something like IDENTITY, depending on the desired semantics of *RETURN* calls outside of a LOAD.