Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RAII in Scheme?

Tags:

raii

lisp

scheme

Is there anyway to implement Resource Acquisation is Initialization in Scheme?

I know that RAII does not work well in GC-ed languages (since we have no idea whe the object is destroyed). However, Scheme has nice things like continuations, dynamic-wind, and closures -- is there a way to use some combination of this to implement RAII?

If not, how do schemers design their code to not use RAII?

[A common example I run into is the following:

I have a 3D mesh, I have a Vertex Buffer Object atached to it, when the Mesh is no longer used, I want the VBO freed up.]

Thanks!

like image 832
anon Avatar asked Jan 19 '10 02:01

anon


1 Answers

If this is just a one-off, you could always just write a macro that wraps around dynamic-wind, doing the setup and teardown in the before and after thunks (I'm assuming that allocate-vertex-buffer-object and free-vertex-buffer-object are your constructor and destructors here):

(define-syntax with-vertex-buffer-object
  (syntax-rules ()
    ((_ (name arg ...) body ...)
     (let ((name #f))
       (dynamic-wind
         (lambda () (set! name (allocate-vertex-buffer-object args ...)))
         (lambda () body ...)
         (lambda () (free-vertex-buffer-object name) (set! name #f)))))))

If this is a pattern that you use a lot, for different types of objects, you might write a macro to generate this sort of macro; and likely you are going to want to allocate a series of these at a time, so you may want to have a list of bindings at the beginning, instead of just a single one.

Here's an off-the cuff, more general version; I'm not really sure about the name, but it demonstrates the basic idea (edited to fix infinite loop in original version):

(define-syntax with-managed-objects
  (syntax-rules ()
    ((_ ((name constructor destructor)) body ...)
     (let ((name #f))
       (dynamic-wind
         (lambda () (set! name constructor))
         (lambda () body ...)
         (lambda () destructor (set! name #f)))))
    ((_ ((name constructor destructor) rest ...)
      body ...)
     (with-managed-objects ((name constructor destructor))
       (with-managed-objects (rest ...)
         body ...)))
    ((_ () body ...)
     (begin body ...))))

And you would use this as follows:

(with-managed-objects ((vbo (allocate-vertex-buffer-object 1 2 3)
                            (free-vertext-buffer-object vbo))
                       (frob (create-frobnozzle 'foo 'bar)
                             (destroy-frobnozzle frob)))
  ;; do stuff ...
  )

Here's an example that demonstrates it working, including exiting and re-entering the scope using continuations (this is a rather contrived example, apologies if the control flow is a bit hard to follow):

(let ((inner-continuation #f))
  (if (with-managed-objects ((foo (begin (display "entering foo\n") 1) 
                                  (display "exiting foo\n")) 
                             (bar (begin (display "entering bar\n") (+ foo 1)) 
                                  (display "exiting bar\n")))
        (display "inside\n")
        (display "foo: ") (display foo) (newline)
        (display "bar: ") (display bar) (newline)
        (call/cc (lambda (inside) (set! inner-continuation inside) #t)))
    (begin (display "* Let's try that again!\n") 
           (inner-continuation #f))
    (display "* All done\n")))

This should print:

entering foo
entering bar
inside
foo: 1
bar: 2
exiting bar
exiting foo
* Let's try that again!
entering foo
entering bar
exiting bar
exiting foo
* All done

call/cc is simply an abbreviation of call-with-current-continuation; use the longer form if your Scheme does not have the shorter one.

Update: As you clarified in your comments, you are looking for a way to manage resources that can be returned out of a particular dynamic context. In this case, you're going to have to use a finalizer; a finalizer is a function that will be called with you object once the GC has proven that it cannot be reached from anywhere else. Finalizers are not standard, but most mature Scheme systems have them, sometimes under different names. For instance, in PLT Scheme, see Wills and Executors.

You should keep in mind that in Scheme, a dynamic context can be re-entered; this differs from most other languages, in which you may be able to exit a dynamic context at any arbitrary point using exceptions, but you cannot re-enter. In my example above, I demonstrated a naïve approach of using dynamic-wind to deallocate resources when you leave the dynamic context, and re-allocate them if you enter again. This may be appropriate for some resources, but for many resources it would not be appropriate (for instance, re-opening a file, you will now be at the beginning of the file when you re-enter the dynamic context), and may have significant overhead.

Taylor Campbell (yes, there is a relation) has an article in his blag (the 2009-03-28 entry) addressing this issue, and presenting a few alternatives based on the exact semantics that you want. For instance, he provides an unwind-protext form that will not call the cleanup procedure until it is no longer possible to re-enter the dynamic context in which the resource is accessible.

So, that covers a lot of different options that are available. There is no exact match to RAII, as Scheme is a very different language and has very different constraints. If you have a more specific use case, or more details on the use case that you mentioned briefly, I may be able to provide you with some more specific advice.

like image 151
Brian Campbell Avatar answered Nov 08 '22 19:11

Brian Campbell