using call/cc with let-assert
This commit is contained in:
@@ -65,8 +65,8 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
(define-syntax let/assert
|
(define-syntax let/assert
|
||||||
(syntax-rules ()
|
(syntax-rules (fail)
|
||||||
((_ fail ((v rest ...) ...) b1 ...)
|
((_ ((v rest ...) ...) b1 ...)
|
||||||
(call/cc
|
(call/cc
|
||||||
(λ (fail)
|
(λ (fail)
|
||||||
(let* ((v (assert-expr fail (rest ...)))
|
(let* ((v (assert-expr fail (rest ...)))
|
||||||
|
|||||||
+182
-122
@@ -3,196 +3,256 @@
|
|||||||
@(require (for-label racket/base
|
@(require (for-label racket/base
|
||||||
"../main.rkt"))
|
"../main.rkt"))
|
||||||
|
|
||||||
@title{let-assert}
|
@title{let/assert}
|
||||||
|
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
@defmodule[let-assert]
|
@defmodule[let-assert]
|
||||||
|
|
||||||
This module provides @racket[let/assert], a small sequential binding form
|
This module provides @racket[let/assert], a small sequential binding
|
||||||
with local assertions. It is useful for defensive programming around FFI
|
form for defensive programming. It is especially useful around FFI calls,
|
||||||
bindings: checks for null pointers, exit codes, and similar failure values can
|
where failure is often reported by ordinary values: @racket[#f] for a
|
||||||
be kept close to the binding that produced them, while the body stays on the
|
null pointer, a negative integer for an error code, or a non-zero integer
|
||||||
happy path. When an assertion fails, the whole @racket[let/assert] expression
|
for a failed C-style status result.
|
||||||
returns the associated fallback value.
|
|
||||||
|
|
||||||
@section{Binding with assertions}
|
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[
|
@defform[
|
||||||
(let/assert (binding ...) body ...+)
|
(let/assert
|
||||||
#:grammar
|
((id expr)
|
||||||
([binding [id expr]
|
(id expr assert-expr failure-expr)
|
||||||
[id expr predicate-expr fallback-expr]])]{
|
...)
|
||||||
|
body ...)
|
||||||
|
]{
|
||||||
|
|
||||||
The form expands to an internal @racket[let*] structure. Bindings are
|
Evaluates the bindings from left to right, using @racket[let*]
|
||||||
therefore evaluated from left to right, and later bindings may refer to
|
semantics. Later bindings may refer to earlier ones.
|
||||||
earlier ones.
|
|
||||||
|
|
||||||
A binding of the form @racket[[id expr]] simply binds @racket[id] to
|
|
||||||
@racket[expr].
|
|
||||||
|
|
||||||
A binding of the form
|
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,
|
@racketblock[
|
||||||
the failing assertion raises a private exception carrying that value; the
|
(id expr)
|
||||||
exception is caught by @racket[let/assert] itself.
|
]
|
||||||
|
|
||||||
|
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.
|
||||||
}
|
}
|
||||||
|
|
||||||
@racketblock[
|
@section{Assertion constructors}
|
||||||
(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)]{
|
@defform[(make-assert name not-name pred)]{
|
||||||
|
|
||||||
Defines two assertion factories in a definition context.
|
Defines two assertion-constructor forms.
|
||||||
|
|
||||||
The form @racket[(name constant)] expands to a unary predicate that applies
|
The generated @racket[name] form creates a predicate that compares its
|
||||||
@racket[pred] to the value being checked and @racket[constant]. The form
|
argument with a constant using @racket[pred]:
|
||||||
@racket[(not-name constant)] expands to the negated variant.
|
|
||||||
|
|
||||||
For example:
|
@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[
|
@racketblock[
|
||||||
(make-assert a-eq? a-!eq? eq?)
|
(make-assert a-eq? a-!eq? eq?)
|
||||||
|
(make-assert a->? a-<=? >)
|
||||||
|
(make-assert a->=? a-<? >=)
|
||||||
|
(make-assert a-=? a-!=? =)
|
||||||
]
|
]
|
||||||
|
|
||||||
defines @racket[a-eq?] and @racket[a-!eq?]. Consequently,
|
The negated forms are logical negations of the positive predicate.
|
||||||
@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? const)]{
|
||||||
|
Creates a predicate that accepts values @racket[eq?] to @racket[const].
|
||||||
@defform[(a-eq? constant)]{
|
|
||||||
Produces a unary predicate that accepts values for which
|
|
||||||
@racket[(eq? value constant)] is true.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(a-!eq? constant)]{
|
@defform[(a-!eq? const)]{
|
||||||
Produces a unary predicate that accepts values for which
|
Creates a predicate that accepts values not @racket[eq?] to
|
||||||
@racket[(eq? value constant)] is false.
|
@racket[const].
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(a->? constant)]{
|
@defform[(a->? const)]{
|
||||||
Produces a unary predicate that accepts values greater than
|
Creates a predicate that accepts values greater than @racket[const].
|
||||||
@racket[constant].
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(a-<=? constant)]{
|
@defform[(a-<=? const)]{
|
||||||
Produces a unary predicate that rejects values greater than
|
Creates the negation of @racket[(a->? const)].
|
||||||
@racket[constant].
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(a->=? constant)]{
|
@defform[(a->=? const)]{
|
||||||
Produces a unary predicate that accepts values greater than or equal to
|
Creates a predicate that accepts values greater than or equal to
|
||||||
@racket[constant].
|
@racket[const].
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(a-<? constant)]{
|
@defform[(a-<? const)]{
|
||||||
Produces a unary predicate that rejects values greater than or equal to
|
Creates the negation of @racket[(a->=? const)].
|
||||||
@racket[constant].
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(a-=? constant)]{
|
@defform[(a-=? const)]{
|
||||||
Produces a unary predicate that accepts values numerically equal to
|
Creates a predicate that accepts values numerically equal to
|
||||||
@racket[constant].
|
@racket[const].
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(a-!=? constant)]{
|
@defform[(a-!=? const)]{
|
||||||
Produces a unary predicate that accepts values not numerically equal to
|
Creates the negation of @racket[(a-=? const)].
|
||||||
@racket[constant].
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@section{Ready-made predicates}
|
@section{Built-in predicates}
|
||||||
|
|
||||||
@defproc[(a-=0? [x number?]) boolean?]{
|
@defproc[(a-=0? [x number?]) boolean?]{
|
||||||
Returns @racket[#t] when @racket[x] is numerically equal to zero.
|
Accepts values numerically equal to zero.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(a-!=0? [x number?]) boolean?]{
|
@defproc[(a-!=0? [x number?]) boolean?]{
|
||||||
Returns @racket[#t] when @racket[x] is not numerically equal to zero.
|
Accepts values not numerically equal to zero.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(a->0? [x real?]) boolean?]{
|
@defproc[(a->0? [x real?]) boolean?]{
|
||||||
Returns @racket[#t] when @racket[x] is greater than zero.
|
Accepts values greater than zero.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(a->=0? [x real?]) boolean?]{
|
@defproc[(a->=0? [x real?]) boolean?]{
|
||||||
Returns @racket[#t] when @racket[x] is greater than or equal to zero.
|
Accepts values greater than or equal to zero.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(a-<0? [x real?]) boolean?]{
|
@defproc[(a-<0? [x real?]) boolean?]{
|
||||||
Returns @racket[#t] when @racket[x] is less than zero.
|
Accepts values less than zero.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(a-<=0? [x real?]) boolean?]{
|
@defproc[(a-<=0? [x real?]) boolean?]{
|
||||||
Returns @racket[#t] when @racket[x] is less than or equal to zero.
|
Accepts values less than or equal to zero.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defthing[a-true? procedure?]{
|
@defthing[a-true? procedure?]{
|
||||||
A predicate equivalent to @racket[(a-eq? #t)].
|
A predicate that accepts only @racket[#t].
|
||||||
}
|
}
|
||||||
|
|
||||||
@defthing[a-false? procedure?]{
|
@defthing[a-false? procedure?]{
|
||||||
A predicate equivalent to @racket[(a-eq? #f)].
|
A predicate that accepts only @racket[#f].
|
||||||
}
|
}
|
||||||
|
|
||||||
@defthing[a-nullptr? procedure?]{
|
@defthing[a-nullptr? procedure?]{
|
||||||
A predicate equivalent to @racket[(a-eq? #f)]. The name is intended for FFI
|
A predicate that accepts @racket[#f]. This is intended for FFI bindings
|
||||||
code where @racket[#f] is used to represent a null pointer.
|
where a native null pointer is represented as @racket[#f].
|
||||||
}
|
}
|
||||||
|
|
||||||
@defthing[a-!nullptr? procedure?]{
|
@defthing[a-!nullptr? procedure?]{
|
||||||
A predicate equivalent to @racket[(a-!eq? #f)]. It accepts values that are
|
A predicate that accepts values that are not @racket[#f]. This is useful
|
||||||
not @racket[#f], which is often the success case for pointer-returning FFI
|
for checking successful pointer allocation in FFI code.
|
||||||
calls.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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.
|
||||||
Reference in New Issue
Block a user