Files
let-assert/scrbl/let-assert.scrbl
T
2026-05-11 23:20:30 +02:00

212 lines
6.4 KiB
Racket

#lang scribble/manual
@(require (for-label racket/base
"../main.rkt"))
@title{let/assert}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[let-assert]
This module provides @racket[let/assert], 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 @racket[let/assert] expression
returns the associated fallback value.
@section{Binding with assertions}
@defform[
(let/assert (binding ...) body ...+)
#:grammar
([binding [id expr]
[id expr predicate-expr fallback-expr]])]{
Evaluates the bindings from left to right and then evaluates the body. Later
bindings may refer to earlier bindings, as with @racket[let*].
The implementation expands through an internal helper into nested
@racket[let] and @racket[cond] forms. There is no exception-based control
flow in this version: a failing assertion directly selects the fallback result
for that binding.
A binding of the form @racket[[id expr]] simply binds @racket[id] to
@racket[expr].
A binding of the form
@racket[[id expr predicate-expr fallback-expr]] evaluates @racket[expr] once,
binds the result to @racket[id], and then calls @racket[predicate-expr] with
that value. If the predicate returns a true value, evaluation continues with
the next binding or with the body. If the predicate returns @racket[#f], the
body is not evaluated and the complete @racket[let/assert] form returns
@racket[fallback-expr].
The fallback expression belongs to the binding where the assertion is made.
This keeps failure handling close to the operation that may fail.
}
@racketblock[
(let/assert ([x 10 (a->? 0) 'too-small]
[y (+ x 2) (a-=? 12) 'wrong-value])
y)
]
This expression returns @racket[12]. The second binding may use @racket[x],
because the bindings are evaluated sequentially.
@racketblock[
(let/assert ([ptr (open-native-handle) a-!nullptr? 'open-failed]
[ret (use-native-handle ptr) (a->=? 0) 'call-failed])
'ok)
]
This style is useful around FFI code. If @racket[open-native-handle] returns
@racket[#f], the result is @racket['open-failed]. If
@racket[use-native-handle] returns a negative status code, the result is
@racket['call-failed]. Otherwise the body is evaluated and the result is
@racket['ok].
@section{FFI-style example}
Many C libraries, including FFmpeg-style APIs, report failure through null
pointers or integer return codes. A typical wrapper can therefore keep the
normal path small:
@racketblock[
(define (open-decoder file)
(let/assert ([ctx (avformat-open-input file)
a-!nullptr?
'open-input-failed]
[ret (avformat-find-stream-info ctx)
(a->=? 0)
'stream-info-failed]
[stream (find-best-audio-stream ctx)
a-!nullptr?
'no-audio-stream])
stream))
]
The example is intentionally written as wrapper-style Racket code rather than
as a direct FFmpeg binding. The important point is the shape: pointer-returning
operations are checked with @racket[a-!nullptr?], while return-code operations
are checked with predicates such as @racket[(a->=? 0)].
@section{Creating assertion factories}
@defform[(make-assert name not-name pred)]{
Defines two assertion factories.
The form @racket[(name constant)] produces a unary predicate that applies
@racket[pred] to the checked value and @racket[constant]:
@racketblock[
(lambda (x) (pred x constant))
]
The form @racket[(not-name constant)] produces the negated variant:
@racketblock[
(lambda (x) (not (pred x constant)))
]
For example:
@racketblock[
(make-assert a-eq? a-!eq? eq?)
]
defines @racket[a-eq?] and @racket[a-!eq?].
}
@section{Assertion factories}
@defform[(a-eq? constant)]{
Produces a predicate that accepts values for which
@racket[(eq? value constant)] is true.
}
@defform[(a-!eq? constant)]{
Produces a predicate that accepts values for which
@racket[(eq? value constant)] is false.
}
@defform[(a->? constant)]{
Produces a predicate that accepts values greater than @racket[constant].
}
@defform[(a-<=? constant)]{
Produces a predicate that accepts values for which @racket[(> value constant)]
is false.
}
@defform[(a->=? constant)]{
Produces a predicate that accepts values greater than or equal to
@racket[constant].
}
@defform[(a-<? constant)]{
Produces a predicate that accepts values for which
@racket[(>= value constant)] is false.
}
@defform[(a-=? constant)]{
Produces a predicate that accepts values numerically equal to
@racket[constant].
}
@defform[(a-!=? constant)]{
Produces a predicate that accepts values not numerically equal to
@racket[constant].
}
The negated factories are exact negations of their corresponding predicate.
For ordinary numeric values, @racket[(a-<=? n)] behaves like a less-than-or-equal
check and @racket[(a-<? n)] behaves like a less-than check, but they are
implemented as negations of @racket[>] and @racket[>=].
@section{Ready-made predicates}
@defproc[(a-=0? [x number?]) boolean?]{
Returns @racket[#t] when @racket[x] is numerically equal to zero.
}
@defproc[(a-!=0? [x number?]) boolean?]{
Returns @racket[#t] when @racket[x] is not numerically equal to zero.
}
@defproc[(a->0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is greater than zero.
}
@defproc[(a->=0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is greater than or equal to zero.
}
@defproc[(a-<0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is less than zero.
}
@defproc[(a-<=0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is less than or equal to zero.
}
@defproc[(a-true? [x any/c]) boolean?]{
Returns @racket[#t] when @racket[x] is @racket[eq?] to @racket[#t].
}
@defproc[(a-false? [x any/c]) boolean?]{
Returns @racket[#t] when @racket[x] is @racket[eq?] to @racket[#f].
}
@defproc[(a-nullptr? [x any/c]) boolean?]{
Returns @racket[#t] when @racket[x] is @racket[eq?] to @racket[#f]. The name
is intended for FFI code where @racket[#f] represents a null pointer.
}
@defproc[(a-!nullptr? [x any/c]) boolean?]{
Returns @racket[#t] when @racket[x] is not @racket[eq?] to @racket[#f]. This
is often the success predicate for FFI calls that return either a native
pointer or @racket[#f].
}