Files
2026-05-10 20:56:22 +02:00

198 lines
5.9 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]])]{
The form expands to an internal @racket[let*] structure. Bindings are
therefore evaluated from left to right, and later bindings may refer to
earlier ones.
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,
applies @racket[predicate-expr] to the result, and binds @racket[id] to that
result when the predicate accepts it. If the predicate returns @racket[#f],
the body is not evaluated and the whole @racket[let/assert] form returns
@racket[fallback-expr].
The fallback expression is evaluated only when the assertion fails. Internally,
the failing assertion raises a private exception carrying that value; the
exception is caught by @racket[let/assert] itself.
}
@racketblock[
(let/assert ([x 10 (a->? 0) 'too-small]
[y (+ x 2) (a-=? 12) 'wrong-value])
y)
]
The example returns @racket[12]. The second binding can use @racket[x],
because @racket[let/assert] has @racket[let*] scoping.
@section{FFI-style examples}
FFI libraries often report errors by returning a null pointer or a negative
integer status code. With @racket[let/assert], those checks can be written
next to the operation that may fail.
@racketblock[
(define (open-ffmpeg-instance)
(let/assert ([fh (fmpg-init) a-!nullptr? #f])
fh))
]
Here @racket[fmpg-init] is expected to return either a valid native handle or
@racket[#f]. If the handle is @racket[#f], the complete
@racket[let/assert] form returns @racket[#f]. Otherwise the valid handle is
returned.
A slightly larger example can combine a pointer check with an FFmpeg-style
return-code check:
@racketblock[
(define (decode-next! ds)
(let/assert ([pkt (av-packet-alloc) a-!nullptr? 'packet-allocation-failed]
[ret (read-selected-audio-packet! ds pkt)
(a->=? 0)
'read-packet-failed]
[pcm (receive-available-frames! ds)
bytes?
'decode-failed])
pcm))
]
The body contains only the successful path. If packet allocation fails, the
result is @racket['packet-allocation-failed]. If reading the packet returns a
negative status code, the result is @racket['read-packet-failed]. If decoding
does not produce bytes, the result is @racket['decode-failed].
@section{Creating assertion factories}
@defform[(make-assert name not-name pred)]{
Defines two assertion factories in a definition context.
The form @racket[(name constant)] expands to a unary predicate that applies
@racket[pred] to the value being checked and @racket[constant]. The form
@racket[(not-name constant)] expands to the negated variant.
For example:
@racketblock[
(make-assert a-eq? a-!eq? eq?)
]
defines @racket[a-eq?] and @racket[a-!eq?]. Consequently,
@racket[(a-eq? #f)] produces a predicate that accepts values that are
@racket[eq?] to @racket[#f], while @racket[(a-!eq? #f)] accepts values that
are not @racket[eq?] to @racket[#f].
}
@section{Assertion factories}
@defform[(a-eq? constant)]{
Produces a unary predicate that accepts values for which
@racket[(eq? value constant)] is true.
}
@defform[(a-!eq? constant)]{
Produces a unary predicate that accepts values for which
@racket[(eq? value constant)] is false.
}
@defform[(a->? constant)]{
Produces a unary predicate that accepts values greater than
@racket[constant].
}
@defform[(a-<=? constant)]{
Produces a unary predicate that rejects values greater than
@racket[constant].
}
@defform[(a->=? constant)]{
Produces a unary predicate that accepts values greater than or equal to
@racket[constant].
}
@defform[(a-<? constant)]{
Produces a unary predicate that rejects values greater than or equal to
@racket[constant].
}
@defform[(a-=? constant)]{
Produces a unary predicate that accepts values numerically equal to
@racket[constant].
}
@defform[(a-!=? constant)]{
Produces a unary predicate that accepts values not numerically equal to
@racket[constant].
}
@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.
}
@defthing[a-true? procedure?]{
A predicate equivalent to @racket[(a-eq? #t)].
}
@defthing[a-false? procedure?]{
A predicate equivalent to @racket[(a-eq? #f)].
}
@defthing[a-nullptr? procedure?]{
A predicate equivalent to @racket[(a-eq? #f)]. The name is intended for FFI
code where @racket[#f] is used to represent a null pointer.
}
@defthing[a-!nullptr? procedure?]{
A predicate equivalent to @racket[(a-!eq? #f)]. It accepts values that are
not @racket[#f], which is often the success case for pointer-returning FFI
calls.
}