Files
define-return/scrbl/define-return-contract.scrbl
T
2026-05-11 17:13:09 +02:00

179 lines
5.7 KiB
Racket

#lang scribble/manual
@(require (for-label racket/base
racket/contract
(only-in ffi/unsafe cpointer?)
"../contract.rkt"))
@title{define/contract/return}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[define-return/contract]
The @racketmodname[define-return/contract] library provides
@racket[define/contract/return], a contracted definition form with an
explicit early return. It is useful in small defensive functions, and
especially around FFI bindings, where null pointers, error codes,
unsupported states, or failed preconditions should leave the function
immediately.
The return mechanism is based on @racket[call/cc]. A function definition
captures the continuation around the function body and binds it to the
return identifier supplied by the programmer. Calling that identifier
leaves the body and returns the supplied value as the result of the
function. No exception is raised and no exception handler is installed.
The module re-exports @racketmodname[racket/contract], so contracts such
as @racket[->], @racket[->*], @racket[any/c], @racket[or/c],
@racket[and/c], and @racket[listof] are available from the same
@racket[require]. It also re-exports @racketmodname[define-return].
Note. This can lead to a clash between @racket[->] from
@racketmodname[racket/contract] and @racket[->] from
@racketmodname[ffi/unsafe].
@section{Contracted definitions}
@defform*[
((define/contract/return (name . formals) return-id
(contract-part ... result-contract)
body ...)
(define/contract/return name
result-contract
value))
]{
Like @racket[define/contract], but the function form gives the body a
local early-return identifier.
The first form defines a contracted function. The @racket[return-id]
identifier is part of the surface syntax. It is not a predefined binding
exported by this module. This keeps the form hygienic: the programmer
chooses the name of the local escape continuation, and the body uses that
same name explicitly.
Ordinary results are checked by @racket[define/contract]. Early-returned
values are checked separately against @racket[result-contract]. The
implementation does this by defining a small local contracted returner
with contract @racket[(-> any/c result-contract)]. The early return value
therefore passes through Racket's contract machinery before it leaves the
function body.
The contract must be written inline as a parenthesized contract form. The
last element is used as the early-return result contract.
@racketblock[
(define/contract/return (h x) return
(-> number? (or/c symbol? number?))
(when (< x 0) (return 'x-not-positive))
(let ((y (* x x)))
(when (> y 100) (return 'maxed-out))
(when (= y 9) (return "Wrong answer!"))
y))
]
Here the result contract is @racket[(or/c symbol? number?)]. The symbol
returns are accepted, ordinary numeric results are accepted, and the
string @racket["Wrong answer!"] is rejected.
@codeblock|{
(h -1) ; => 'x-not-positive
(h 2) ; => 4
(h 11) ; => 'maxed-out
(h 3) ; contract violation: string result
}|
A zero-argument function works in the same way:
@racketblock[
(define/contract/return (z) return
(-> symbol?)
(let ((cs (current-seconds)))
(when (= (remainder cs 3) 0) (return "divisible by 3"))
(when (= (remainder cs 2) 0) (return 'divisible-by-2))
'yes))
]
The result contract is @racket[symbol?]. Returning
@racket['divisible-by-2] or @racket['yes] is accepted. Returning the
string @racket["divisible by 3"] is rejected.
Rest arguments can be used when the corresponding contract form is
accepted by @racket[define/contract]:
@racketblock[
(define/contract/return (sum . xs) return
(->* () #:rest (listof number?) number?)
(when (null? xs) (return 0))
(apply + xs))
]
@codeblock|{
(sum) ; => 0
(sum 1 2) ; => 3
(sum 1 'x) ; contract violation
}|
Since @racket[return-id] is a continuation wrapped in a local contracted
procedure, it is called with one value. The continuation should normally
be used only to escape from the dynamic extent of the function body.
The second form defines a contracted value. The value is checked against
@racket[result-contract]. This form has no @racket[return-id] position,
so it does not expose a local early-return identifier. Use the function
form when a body needs an explicit early return.
@racketblock[
(define/contract/return answer
number?
42)
]
This definition succeeds because @racket[42] satisfies
@racket[number?].
@racketblock[
(define/contract/return bad-answer
number?
'not-a-number)
]
This definition raises a contract violation, because
@racket['not-a-number] does not satisfy @racket[number?].
}
@section{FFI-style null pointers}
Many FFI bindings represent a native null pointer as @racket[#f]. The
explicit return identifier makes it easy to leave a wrapper as soon as a
required pointer is missing, while still checking the final result against
the function contract.
@racketblock[
(define/contract/return (make-stream-codec-context stream) return
(-> cpointer? (or/c cpointer? symbol?))
(define codecpar (AVStream-codecpar stream))
(unless codecpar
(return 'missing-codec-parameters))
(define codec
(avcodec-find-decoder
(AVCodecParameters-codec-id codecpar)))
(unless codec
(return 'decoder-not-found))
(define ctx (avcodec-alloc-context3 codec))
(unless ctx
(return 'alloc-codec-context-failed))
ctx)
]
The checks are sequential. The decoder lookup is evaluated only when
@racket[codecpar] is a valid pointer, and the context allocation is
evaluated only when @racket[codec] is a valid pointer. A failed pointer
check returns the corresponding symbol immediately. A successful path
returns @racket[ctx], and both the ordinary result and early-returned
symbols are covered by the result contract.