RAII in Scheme?
Asked Answered
R

1

8

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!

Rush answered 19/1, 2010 at 2:46 Comment(4)
Hi, anon. I'm wondering if my answer has satisfied you, or if you're looking for something else.Cromagnon
I think your response is as good as it gets given it's scheme. We at some level, we have to know when the model "dies" and gives up it's vbo. However, in RAII + GC, I we don't need to know this in advance, we can say "Model, I don't know when you will die; but I know that when you do, you'll give up the VBO". We can't quite do the later because scheme is gc-ed; what I was originally hoping for ... was some type of clever macro mack that automatically interleaved some type of ref-counting in, which would they provide that type of RAII + Refcounting.Rush
To further add to this, consider the following situation: we create a Model, we don't know when it's deleted, but we know it's rendered alot; so we give it a VBO; pass it around alot; ... and when no one uses it, it frees up the VBO. There's not a single place in the code where I know "I can now free the model."Rush
Ah, OK. I wasn't aware of exactly what aspect of RAII you were referring to; the term is a bit vague, and can be used for allocating resources with a well-defined lifetime safely with respect to exceptions, which is what I addressed. If you need objects of arbitrary lifetime, you will need to use some sort of finalizer; the standard doesn't address finalization, but most relatively mature Schemes have some way of defining finalizers. I will expand my answer to address your questions.Cromagnon
C
15

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.

Cromagnon answered 19/1, 2010 at 2:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.