Files
2026-05-12 08:17:13 +02:00

278 lines
8.4 KiB
Racket

#lang scribble/manual
@(require (for-label racket/base
"../main.rkt"))
@title{Early Return}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[early-return]
This module provides two small control-flow forms. The
@racket[early-return] form is a local, structured early-exit form that
expands to ordinary @racket[let] and @racket[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 @racket[define/return], which gives a function a
named return procedure by using @racket[call/cc]. That form is more general,
but also a heavier control-flow mechanism. Prefer @racket[early-return]
when the desired exit is local to one expression sequence.
@section{Guarded sequential evaluation}
@defform*[
[(early-return
(clause ...)
body ...)]
]{
Evaluates a sequence of @racket[clause]s from left to right. If no guard
clause returns early, the @racket[body] expressions are evaluated and the
value of the last body expression is returned.
The form behaves like a guarded @racket[let*]. Bindings introduced by an
earlier clause are visible in later clauses and in the final body. The body
is placed in a @racket[let] context, so internal definitions may be used.
The following clause forms are recognized:
@racketblock[
(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
@racketblock[
(id expr)
]
binds @racket[id] to the result of @racket[expr] and continues with the next
clause.
A guarded binding clause
@racketblock[
(id expr ? test-expr -> result-expr)
]
first binds @racket[id] to @racket[expr]. The @racket[test-expr] is then
evaluated in a scope where @racket[id] is available. If the test is true,
@racket[result-expr] becomes the result of the whole @racket[early-return]
form. Otherwise evaluation continues with the next clause.
A guarded binding clause with cleanup
@racketblock[
(id expr ? test-expr -> result-expr ~ cleanup-expr)
]
works the same way, except that @racket[cleanup-expr] is evaluated before
@racket[result-expr] when the test is true. The value of
@racket[cleanup-expr] is ignored.
A guard clause without a binding
@racketblock[
(? test-expr -> result-expr)
]
checks @racket[test-expr] immediately. If the test is true,
@racket[result-expr] becomes the result of the whole form.
The cleanup variant
@racketblock[
(? test-expr -> result-expr ~ cleanup-expr)
]
evaluates @racket[cleanup-expr] before returning @racket[result-expr].
Finally,
@racketblock[
(do expr ...)
]
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 @racket[?], @racket[->], @racket[~], and @racket[do] are
literal keywords of the form.
}
@section{Examples}
A simple validation function can be written as a sequence of guards followed
by the actual computation:
@codeblock{
(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:
@racketblock[
(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.
@racketblock[
(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. @racket[early-return] is designed for explicit status-code
control flow, not for exception handling.
@section{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.
@racketblock[
(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 @racket[let] and @racket[cond] code; no continuation is captured.
@section{Named return}
@defform[
(define/return (id arg ...) return-id
body ...)
]{
Defines a function like @racket[define], but binds @racket[return-id] inside
the function body to an escape procedure. Calling @racket[return-id] exits
the function immediately with the supplied value.
@racketblock[
(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 @racket[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
@racket[early-return].
}