Context managers are things that are probably well known in the python community while it might not be well known that the use of the keyword “with” is actually a way to use a context manager. The
with statement has been introduced in python 2.5 in the pep-0343. The context managers define two functions:
In Scheme, we could probably get away with something closer to the PEP 310. We'd have a function that returns a pair of two functions.
The code to use could end up looking like this.
1 2 3 4 5 6 7 8
(with (open "file.txt") as file (print (readline file))) (with (accept-connection port) as (input output) (handle-request input output)) (with (error-context) (/ 1 0))
The code to acheive this should be fairly simple and can make the code much more easier to read in the end. The base function to use should look like this:
1 2 3 4 5 6 7 8
(define (__with__ context body) (let ((values ((car context))) (exit (cdr context))) (exit (apply body (if (pair? values) values (list values))))))
The code above is way to simple for real use because it doesn't handle any kind of errors. Usually we would like to execute the
exit function from within an errorhandler. But for the purpose of making things easy to understand, we will skip that for now.
Now the interesting part is to create a context manager. The open context manager that I created above could be written as such.
1 2 3 4 5 6 7 8 9 10
(define (open filename) (define file (void)) (let ((enter (lambda () (set! file (open-input-file filename)) file)) (exit (lambda values (close-input-port file)))) (cons enter exit)))
When we enter the context manager, we open an input port and set a variable inside our closure. Then when we leave the context manager, it should close the port that we kept. Hopefuly, when we leave the with call, anything inside the context closure should get deleted or garbage collected.
The code above is unfortunately quite simple and there are probably a lot of problems. It is possible to reenter multiple time the same context manager if we set the context manager to a variable. In the case above, it could recreate a new file port without closing the previous file port. This could lead to undesirable behaviors. Putting that aside, lets look how to define a macro for this.
A normal call would look like this:
1 2 3
(__with__ (open "filename") (lambda (file) (print (readline file))))
A macro could look like this:
1 2 3 4
(define-syntax with (syntax-rules (as) ((_ context as arg body ...) (__with__ context (lambda (arg) body ...)))))
If we want to support multiple parameters, we'll have to add some more new rules:
1 2 3 4 5 6 7 8
(define-syntax with (syntax-rules (as) (((_ context as (args ...) body ...) (__with__ context (lambda (args ...) body ...))) ((_ context as arg body ...) (__with__ context (lambda (arg) body ...))) ((_ contex body ...) (__with__ context (lambda vals body ...))))))
The first rule should be able to support calls with multiple arguments as return value. The second rules check for one argument and the last rule catch anything else that doesn't have arguments.
Last word, this looks good but in practice it doesn't handle a lot of edge case yet. I'm not even sure if there is a way to implement context managers such as in python because in python any kind of object can become a context manager as long as it has a
__exit__ function. In Scheme, any context manager should get define per type. On the other hand, defining everything explicitly is pretty much how it works in scheme. One way to get it to work would be to define some “magic” functions like the “toString”. Each type that can be used as a context manager would have to implement those functions. The state of the context manager could be saved in a different closure and that would probably work.