Files
let-assert/scrbl/let-assert.scrbl
T
2026-05-11 17:00:46 +02:00

258 lines
6.3 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 for defensive programming. It is especially useful around FFI calls,
where failure is often reported by ordinary values: @racket[#f] for a
null pointer, a negative integer for an error code, or a non-zero integer
for a failed C-style status result.
The form is implemented with @racket[call/cc]. A failed assertion does
not raise an exception. Instead, it invokes an internal escape
continuation and returns the supplied failure value as the result of the
whole @racket[let/assert] expression.
@defform[
(let/assert
((id expr)
(id expr assert-expr failure-expr)
...)
body ...)
]{
Evaluates the bindings from left to right, using @racket[let*]
semantics. Later bindings may refer to earlier ones.
A binding of the form
@racketblock[
(id expr)
]
is a plain sequential binding.
A binding of the form
@racketblock[
(id expr assert-expr failure-expr)
]
evaluates @racket[expr] once and applies @racket[assert-expr] to the
result. If the assertion succeeds, @racket[id] is bound to the original
value. If the assertion fails, @racket[failure-expr] is returned as the
result of the whole @racket[let/assert] form.
The escape continuation is internal. It is not part of the surface syntax
and is not visible in the body.
@racketblock[
(define (half-positive n)
(let/assert
((x n a->0? 'not-positive))
(/ x 2)))
]
The result is @racket[5] for @racket[(half-positive 10)] and
@racket['not-positive] for @racket[(half-positive 0)].
Because @racket[let/assert] uses @racket[call/cc], failure is local
control flow. There is no exception object, no exception handler, and no
dynamic catching of unrelated failures.
}
@section{Assertion constructors}
@defform[(make-assert name not-name pred)]{
Defines two assertion-constructor forms.
The generated @racket[name] form creates a predicate that compares its
argument with a constant using @racket[pred]:
@racketblock[
((name const) value)
]
is equivalent to:
@racketblock[
(λ (x) (pred x const))
]
The generated @racket[not-name] form creates the negated predicate:
@racketblock[
(λ (x) (not (pred x const)))
]
For example, the module defines:
@racketblock[
(make-assert a-eq? a-!eq? eq?)
(make-assert a->? a-<=? >)
(make-assert a->=? a-<? >=)
(make-assert a-=? a-!=? =)
]
The negated forms are logical negations of the positive predicate.
}
@defform[(a-eq? const)]{
Creates a predicate that accepts values @racket[eq?] to @racket[const].
}
@defform[(a-!eq? const)]{
Creates a predicate that accepts values not @racket[eq?] to
@racket[const].
}
@defform[(a->? const)]{
Creates a predicate that accepts values greater than @racket[const].
}
@defform[(a-<=? const)]{
Creates the negation of @racket[(a->? const)].
}
@defform[(a->=? const)]{
Creates a predicate that accepts values greater than or equal to
@racket[const].
}
@defform[(a-<? const)]{
Creates the negation of @racket[(a->=? const)].
}
@defform[(a-=? const)]{
Creates a predicate that accepts values numerically equal to
@racket[const].
}
@defform[(a-!=? const)]{
Creates the negation of @racket[(a-=? const)].
}
@section{Built-in predicates}
@defproc[(a-=0? [x number?]) boolean?]{
Accepts values numerically equal to zero.
}
@defproc[(a-!=0? [x number?]) boolean?]{
Accepts values not numerically equal to zero.
}
@defproc[(a->0? [x real?]) boolean?]{
Accepts values greater than zero.
}
@defproc[(a->=0? [x real?]) boolean?]{
Accepts values greater than or equal to zero.
}
@defproc[(a-<0? [x real?]) boolean?]{
Accepts values less than zero.
}
@defproc[(a-<=0? [x real?]) boolean?]{
Accepts values less than or equal to zero.
}
@defthing[a-true? procedure?]{
A predicate that accepts only @racket[#t].
}
@defthing[a-false? procedure?]{
A predicate that accepts only @racket[#f].
}
@defthing[a-nullptr? procedure?]{
A predicate that accepts @racket[#f]. This is intended for FFI bindings
where a native null pointer is represented as @racket[#f].
}
@defthing[a-!nullptr? procedure?]{
A predicate that accepts values that are not @racket[#f]. This is useful
for checking successful pointer allocation in FFI code.
}
@section{Examples}
A simple status-code check:
@racketblock[
(define (open-status r)
(let/assert
((status r a-=0? 'open-failed))
'opened))
]
Here @racket[(open-status 0)] returns @racket['opened], while
@racket[(open-status -1)] returns @racket['open-failed].
A pointer-style check:
@racketblock[
(define (use-context maybe-ctx)
(let/assert
((ctx maybe-ctx a-!nullptr? 'no-context))
ctx))
]
Here @racket[#f] is treated as a failed native pointer result.
Plain bindings and asserted bindings may be mixed. Since the form uses
@racket[let*] semantics, later expressions can refer to earlier values:
@racketblock[
(define (make-scaled-size width height scale)
(let/assert
((w width a->0? 'bad-width)
(h height a->0? 'bad-height)
(s scale a->0? 'bad-scale)
(area (* w h)))
(* area s)))
]
The expression @racket[area] is only evaluated after @racket[w] and
@racket[h] have passed their assertions.
@subsection{FFmpeg-style null pointers}
Many FFmpeg functions return a native pointer, where @racket[#f]
represents a null pointer. Since @racket[let/assert] uses sequential
@racket[let*] semantics, later bindings may depend on earlier checked
pointer results.
@racketblock[
(define (make-stream-codec-context stream)
(let/assert
((codecpar (AVStream-codecpar stream) a-!nullptr?
'missing-codec-parameters)
(codec (avcodec-find-decoder
(AVCodecParameters-codec-id codecpar))
a-!nullptr?
'decoder-not-found)
(ctx (avcodec-alloc-context3 codec) a-!nullptr?
'alloc-codec-context-failed))
ctx))
]
If @racket[avcodec-find-decoder] returns @racket[#f], the whole
@racket[let/assert] expression returns @racket['decoder-not-found].
The second binding is then not evaluated.
If @racket[codec] is a valid pointer, it is used by the next binding.
If @racket[avcodec-alloc-context3] returns @racket[#f], the expression
returns @racket['alloc-codec-context-failed]. Otherwise @racket[ctx] is
bound to the allocated context and returned by the body.