Early Return
| (require early-return) | package: early-return |
This module provides two small control-flow forms. The early-return form is a local, structured early-exit form that expands to ordinary let and cond expressions. It is intended for guard-style code, especially around FFI bindings where null pointers, negative return values, failed allocations, or other explicit status values must be checked immediately.
The module also provides define/return, which gives a function a named return procedure by using call/cc. That form is more general, but also a heavier control-flow mechanism. Prefer early-return when the desired exit is local to one expression sequence.
1 Guarded sequential evaluation
syntax
(early-return (clause ...) body ...)
The form behaves like a guarded let*. Bindings introduced by an earlier clause are visible in later clauses and in the final body. The body is placed in a let context, so internal definitions may be used.
The following clause forms are recognized:
(id expr) (id expr ? test-expr -> result-expr) (id expr ? test-expr -> result-expr ~ cleanup-expr) (? test-expr -> result-expr) (? test-expr -> result-expr ~ cleanup-expr) (do expr ...)
A plain clause
(id expr)
binds id to the result of expr and continues with the next clause.
A guarded binding clause
(id expr ? test-expr -> result-expr)
first binds id to expr. The test-expr is then evaluated in a scope where id is available. If the test is true, result-expr becomes the result of the whole early-return form. Otherwise evaluation continues with the next clause.
A guarded binding clause with cleanup
(id expr ? test-expr -> result-expr ~ cleanup-expr)
works the same way, except that cleanup-expr is evaluated before result-expr when the test is true. The value of cleanup-expr is ignored.
A guard clause without a binding
(? test-expr -> result-expr)
checks test-expr immediately. If the test is true, result-expr becomes the result of the whole form.
The cleanup variant
(? test-expr -> result-expr ~ cleanup-expr)
evaluates cleanup-expr before returning result-expr.
Finally,
evaluates one or more expressions for their side effects and then continues with the next clause. This is useful for operations that must happen between guarded bindings, such as setting a pointer, updating state, or logging a value.
The identifiers ?, ->, ~, and do are literal keywords of the form.
2 Examples
A simple validation function can be written as a sequence of guards followed by the actual computation:
(define (square-small-number x) (early-return ((? (not (number? x)) -> 'not-a-number) (? (< x 0) -> 'negative) (v (* x x) ? (> v 100) -> 'too-big)) v)) (square-small-number "x") ; => 'not-a-number (square-small-number -1) ; => 'negative (square-small-number 5) ; => 25 (square-small-number 20) ; => 'too-big
Bindings are sequential. A later clause can use values introduced by earlier clauses:
(define (h x) (early-return ((? (not (number? x)) -> 'not-a-number) (z (+ x x)) (do (displayln (format "z = ~a" z))) (v (* x x) ? (> v 100) -> 'too-big) (do (displayln (format "v = ~a" v))) (g (+ v 10) ? (< g 25) -> 'too-small) (do (displayln (format "g = ~a" g)))) (+ g v 100 z)))
The form is especially useful around FFI code. In the following example, two native blocks are allocated. If the second allocation fails, the first block is freed. Later failure paths use a local cleanup procedure.
(define (copy-native-block src nbytes) (early-return ((tmp (malloc nbytes 'raw) ? (eq? tmp #f) -> #f) (out (malloc pointer 1 'raw) ? (eq? out #f) -> #f ~ (free tmp)) (cleanup! (λ () (unless (eq? out #f) (free out)) (unless (eq? tmp #f) (free tmp)))) (do (ptr-set! out pointer 0 tmp)) (copied? (copy-from-native! tmp src nbytes) ? (not copied?) -> #f ~ (cleanup!)) (result (make-result tmp nbytes) ? (not result) -> #f ~ (cleanup!))) (cleanup!) result))
The cleanup expressions belong to the guard where they are written. They do not create an exception-safe resource scope. If an expression raises a Racket exception, the cleanup expressions in later guards are not evaluated. For exception-safe cleanup, use Racket’s exception and dynamic-wind facilities. early-return is designed for explicit status-code control flow, not for exception handling.
3 A resampler drain example
The following example shows the intended style for FFI-style code that must check return values and clean up native memory explicitly.
(define (drain-resampler! self) (let* ((dec (fmpg-instance-decoder self)) (info (fmpg-instance-audio-info self)) (channels (ais-channels info)) (sample-rate (ais-rate info)) (continue (gensym 'continue))) (define (drain-once! delay max-bytes produced) (early-return ((tmp (malloc max-bytes 'raw) ? (eq? tmp #f) -> -1) (out-planes (malloc pointer 1 'raw) ? (eq? out-planes #f) -> -1 ~ (free tmp)) (cleanup! (λ () (unless (eq? out-planes #f) (free out-planes)) (unless (eq? tmp #f) (free tmp)))) (do (ptr-set! out-planes pointer 0 tmp)) (out-samples (swr_convert (ds-swr-ctx dec) out-planes delay #f 0) ? (<= out-samples 0) -> produced ~ (cleanup!)) (used-bytes (av_samples_get_buffer_size #f channels out-samples FMPG_OUTPUT_FMT 1) ? (< used-bytes 0) -> produced ~ (cleanup!)) (do (when (pcm-empty? dec) (ds-start-sample! dec (ds-next-sample-pos dec)) (ds-timecode! dec (/ (exact->inexact (ds-start-sample dec)) (exact->inexact sample-rate))))) (appended? (append-bytes! dec tmp used-bytes) ? (not appended?) -> -1 ~ (cleanup!)) (do (ds-last-samples! dec (+ (ds-last-samples dec) out-samples)) (ds-next-sample-pos! dec (+ (ds-next-sample-pos dec) out-samples)) (cleanup!))) continue)) (let loop ((produced 0)) (early-return ((delay (swr_get_delay (ds-swr-ctx dec) sample-rate) ? (<= delay 0) -> produced) (max-bytes (av_samples_get_buffer_size #f channels delay FMPG_OUTPUT_FMT 1) ? (<= max-bytes 0) -> produced) (r (drain-once! delay max-bytes produced) ? (not (eq? r continue)) -> r)) (loop 1)))))
This keeps the ordinary success path at the bottom of the form and puts each failure path next to the operation that can fail. The generated code is just nested let and cond code; no continuation is captured.
4 Named return
syntax
(define/return (id arg ...) return-id body ...)
(define/return (classify x) return (when (not (number? x)) (return 'not-a-number)) (when (< x 0) (return 'negative)) (when (> x 100) (return 'too-large)) 'ok)
This form is implemented with call/cc. It is useful when a real non-local escape procedure is wanted, but it is more general than necessary for simple sequential guard code. For ordinary local checks, prefer early-return.