#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)]. } @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.