A small sequential binding form with local assertions. It is useful for defensive programming around FFI bindings: checks for null pointers, exit codes, and similar failure values can be kept close to the binding that produced them, while the body stays on the happy path. When an assertion fails, the whole let/assert expression returns the associated fallback value.
Example of use in racket-audio (under development):
The define-return library provides definition forms with an explicit early return. This is useful in small defensive functions, and especially around FFI bindings, where null pointers, error codes, unsupported states, or failed preconditions should leave the function immediately.
Interesting. Your implementation seems to use special Racket features.
In Scheme+ for Racket there is an implementation using scheme only. (see doc and code and optionally this code)
This makes me reconsider my implementation. I'll probably make it more intuĂŻtive if I choose the call/cc, which then will be more syntactic sugar around let/ec etc.
No call/cc nor exceptions used anymore. I didn't like the idea of the call/cc overhead.
Simple dumb example:
> (define (h x)
(early-return
((? (not (number? x)) -> 'not-a-number ~ (displayln (format "cleaning up with (h 8): ~a" (h 8))))
(z (+ x x))
(do (displayln (format "z = ~a" z)))
(v (* x x) ? (> v 100) -> 'too-big)
(do (displayln (format "v = ~a" v)))
(do (define (f x) (/ x 3)))
(g (+ v 10) ? (< g 25) -> 'too-small)
(do (displayln (format "g = ~a" g)))
)
(f (+ g v 100 z))))
> (h 34)
z = 68
'too-big
> (h 3)
z = 6
v = 9
'too-small
> (h 8)
z = 16
v = 64
g = 74
84 2/3
> (h 'no-num)
z = 16
v = 64
g = 74
cleaning up with (h 8): 254/3
'not-a-number
>
You might be interested in the guard library by @notjack. It is also implemented without continuation-related features.
If you did use continuation-related features, call-with-continuation-prompt/abort-current-continuation could be an alternative to let/ec. However …
You can avoid much of the need for early return by writing more idiomatic Racket.
Unlike the C function of the same name, malloc will only return #f if you have requested an an allocation of size zero. Out-of-memory errors raise an exn:fail:out-of-memory, instead.
Furthermore, you won't need cleanup! if you avoid allocating in 'raw mode: either 'atomic-interior or (IIUC in your case) 'atomic mode will let the memory be GCed normally.
You might be able to go further: a Racket byte string can be used directly as a cpointer?, and I'm not sure if out-planes really needs to be allocated separately from tmp (but maybe I just haven't followed closely enough).
Since much of this is a loop, you might also try to fit it into for/fold, which comes with features like #:break if needed.
I wasn't aware of the out-of-memory exn, thought malloc was more low level, following C-semantics.
I'm currently using malloc 'raw / free, because I know that this memory can be deleted and I suppose this could be more efficient than letting the GC handle it. As this is part of an audio pipeline, I would want is to be as efficient as possible. Is my assumption correct?
I'll look into for/fold.
Do you have an example of 'idomatic racket'? I'm using early returns, because this checks boxes and leaves me with in a checked state for the 'good path'.
With the caveat that everything is relative and that there’s no substitute for benchmarking: I would expect using 'raw mode would be less efficient than using a malloc mode that cooperates with the GC.
When you call Racket’s malloc in 'raw mode, Racket has to really call the C malloc() function, because the semantics of 'raw mode allow ownership can be transferred to a foreign library that will free() it from C later. Typical C malloc() implementations (there is a whole world of alternative C malloc()s …) are not tuned to make small, short-lived allocations efficiently. This is why C programmers typically avoid heap allocations when possible in favor of stack allocation, especially when performance is a concern. (Racket will also incur a little extra overhead from having to switch to C calling conventions, but this is small and improving: I wouldn’t worry about it.)
In contrast, Chez Scheme’s “automatic storage management system” is tuned to make small, short-lived allocations efficiently. (This discussion is an example of why I’ve come to appreciate Dybvig et al.’s use of “automatic storage management system” when they are talking about more than the GC per se: the allocator matters as much as the collector.) In the optimal case, it will simply increment an allocation pointer, which its custom calling convention even keeps in a machine register on most architectures. If you need to use 'atomic-interior mode (i.e. if the memory must not be moved), there would be some more overhead, but I’d expect it to be more efficient than C malloc(). And reclaiming space from a short-lived allocation just means that it isn’t copied out of the nursery.
My rule of thumb would be to only use 'raw mode when you specifically need to interoperate with free() from C.
Allocation does have costs, and allocating less will typically perform better than allocating more, but cooperating with the automatic storage management system is usually a good idea.
Something else to think about is that, if your whole drain-resampler! function isn’t running in “uninterruptable mode” (or stronger), it might be killed without ever running cleanup!. The ffi/unsafe/alloc library provides abstractions that manage finalization, typically used in conjustion with ffi/unsafe/define for libraries that provide functions like make_foo()/free_foo(). You could use the same tools to abstract over 'raw mode, but that would introduce even more overhead compared to just using 'atomic mode.
A coincidental quirk is that they both use the racket/class object system extensively. The db implementation is IMO a very elegant use: the db library doesn’t expose an object-oriented interface to users, but it uses object orientation internally to help with supporting multiple backends. (For example, the #:use-place argument works the same way for sqlite3-connect and odbc-connect, both being FFI-based rather than wire-based connections.) In racket/draw, on the other hand, some uses of OOP are because the library was formerly implemented in C++: the dc<%> interface makes sense, but I don’t think many of us would advocate making color% a class in a new design.
Another quirk is that db/private/pre, with the SQLite backend, is used to implement package management (specifically, pkg/db) and the local documentation database: that’s why the SQLite backend lives in the main Git repository, instead of in GitHub - racket/db · GitHub. It thus cannot depend on any non-base packages. The constraint for racket/draw is less dramatic, but it’s still in a somewhat different position than a package not limited to dependencies in the main distribution.