call/cc variant of define/return

This commit is contained in:
2026-05-11 17:13:09 +02:00
parent 2fb61721f9
commit 0bbda2e6d6
2 changed files with 147 additions and 86 deletions
+99 -39
View File
@@ -2,35 +2,40 @@
@(require (for-label racket/base @(require (for-label racket/base
racket/contract racket/contract
(only-in ffi/unsafe cpointer?)
"../contract.rkt")) "../contract.rkt"))
@title{define/return/contract} @title{define/contract/return}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[define-return/contract] @defmodule[define-return/contract]
The @racketmodname[define-return] library provides definition forms with an The @racketmodname[define-return/contract] library provides
explicit early return. This is useful in small defensive functions, and @racket[define/contract/return], a contracted definition form with an
especially around FFI bindings, where null pointers, error codes, unsupported explicit early return. It is useful in small defensive functions, and
states, or failed preconditions should leave the function immediately. especially around FFI bindings, where null pointers, error codes,
unsupported states, or failed preconditions should leave the function
immediately.
The early return is implemented with an internal exception. A @racket[return] The return mechanism is based on @racket[call/cc]. A function definition
raises that exception, and the definition forms catch it around the function captures the continuation around the function body and binds it to the
body. The contracted form additionally checks early-returned values against return identifier supplied by the programmer. Calling that identifier
the result contract. leaves the body and returns the supplied value as the result of the
function. No exception is raised and no exception handler is installed.
This module provides the contracted version of @racket{define/return}. The module re-exports @racketmodname[racket/contract], so contracts such
The module re-exports @racketmodname[racket/contract], so contracts such as as @racket[->], @racket[->*], @racket[any/c], @racket[or/c],
@racket[->], @racket[->*], @racket[any/c], @racket[or/c],
@racket[and/c], and @racket[listof] are available from the same @racket[and/c], and @racket[listof] are available from the same
@racket[require]. @racket[require]. It also re-exports @racketmodname[define-return].
Note. This can lead to clashes of symbol @racket[->] with module @racketmodname[ffi/unsafe]. Note. This can lead to a clash between @racket[->] from
@racketmodname[racket/contract] and @racket[->] from
@racketmodname[ffi/unsafe].
@section{Contracted definitions} @section{Contracted definitions}
@defform*[ @defform*[
((define/contract/return (name . formals) ((define/contract/return (name . formals) return-id
(contract-part ... result-contract) (contract-part ... result-contract)
body ...) body ...)
(define/contract/return name (define/contract/return name
@@ -38,19 +43,27 @@ Note. This can lead to clashes of symbol @racket[->] with module @racketmodname[
value)) value))
]{ ]{
Like @racket[define/contract], but the body may use @racket[return]. Like @racket[define/contract], but the function form gives the body a
local early-return identifier.
The first form defines a contracted function. Ordinary results are checked by The first form defines a contracted function. The @racket[return-id]
@racket[define/contract]. Early-returned values are checked separately identifier is part of the surface syntax. It is not a predefined binding
against @racket[result-contract]. The implementation does this by defining a exported by this module. This keeps the form hygienic: the programmer
small local contracted returner with the same result contract, so the result chooses the name of the local escape continuation, and the body uses that
contract is passed through Racket's contract machinery. 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 The contract must be written inline as a parenthesized contract form. The
last element is used as the early-return result contract. last element is used as the early-return result contract.
@racketblock[ @racketblock[
(define/contract/return (h x) (define/contract/return (h x) return
(-> number? (or/c symbol? number?)) (-> number? (or/c symbol? number?))
(when (< x 0) (return 'x-not-positive)) (when (< x 0) (return 'x-not-positive))
(let ((y (* x x))) (let ((y (* x x)))
@@ -60,8 +73,8 @@ last element is used as the early-return result contract.
] ]
Here the result contract is @racket[(or/c symbol? number?)]. The symbol Here the result contract is @racket[(or/c symbol? number?)]. The symbol
returns are accepted, ordinary numeric results are accepted, and the string returns are accepted, ordinary numeric results are accepted, and the
@racket["Wrong answer!"] is rejected. string @racket["Wrong answer!"] is rejected.
@codeblock|{ @codeblock|{
(h -1) ; => 'x-not-positive (h -1) ; => 'x-not-positive
@@ -73,23 +86,23 @@ returns are accepted, ordinary numeric results are accepted, and the string
A zero-argument function works in the same way: A zero-argument function works in the same way:
@racketblock[ @racketblock[
(define/contract/return (z) (define/contract/return (z) return
(-> symbol?) (-> symbol?)
(let ((cs (current-seconds))) (let ((cs (current-seconds)))
(when (= (remainder cs 3) 0) (return "deelbaar door 3")) (when (= (remainder cs 3) 0) (return "divisible by 3"))
(when (= (remainder cs 2) 0) (return 'dividable-by-2)) (when (= (remainder cs 2) 0) (return 'divisible-by-2))
'yes)) 'yes))
] ]
The result contract is @racket[symbol?]. Returning The result contract is @racket[symbol?]. Returning
@racket['dividable-by-2] or @racket['yes] is accepted. Returning the string @racket['divisible-by-2] or @racket['yes] is accepted. Returning the
@racket["deelbaar door 3"] is rejected. string @racket["divisible by 3"] is rejected.
Rest arguments can be used when the corresponding contract form is accepted Rest arguments can be used when the corresponding contract form is
by @racket[define/contract]: accepted by @racket[define/contract]:
@racketblock[ @racketblock[
(define/contract/return (sum . xs) (define/contract/return (sum . xs) return
(->* () #:rest (listof number?) number?) (->* () #:rest (listof number?) number?)
(when (null? xs) (return 0)) (when (null? xs) (return 0))
(apply + xs)) (apply + xs))
@@ -101,18 +114,65 @@ by @racket[define/contract]:
(sum 1 'x) ; contract violation (sum 1 'x) ; contract violation
}| }|
The second form defines a contracted value. The value expression may use Since @racket[return-id] is a continuation wrapped in a local contracted
@racket[return], and the returned value is checked against procedure, it is called with one value. The continuation should normally
@racket[result-contract]. 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[ @racketblock[
(define/contract/return v (define/contract/return answer
number? number?
(return 'ss)) 42)
] ]
This definition raises a contract violation, because @racket['ss] does not This definition succeeds because @racket[42] satisfies
satisfy @racket[number?]. @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.
+48 -47
View File
@@ -1,7 +1,6 @@
#lang scribble/manual #lang scribble/manual
@(require (for-label racket/base @(require (for-label racket/base
racket/contract
"../main.rkt")) "../main.rkt"))
@title{define/return} @title{define/return}
@@ -9,41 +8,32 @@
@defmodule[define-return] @defmodule[define-return]
The @racketmodname[define-return] library provides definition forms with an The @racketmodname[define-return] library provides @racket[define/return],
explicit early return. This is useful in small defensive functions, and a small definition form with an explicit early return. It is useful in
especially around FFI bindings, where null pointers, error codes, unsupported compact defensive functions, and especially around FFI bindings, where a
states, or failed preconditions should leave the function immediately. null pointer, an error code, an unsupported state, or a failed precondition
should leave the function immediately.
The early return is implemented with an internal exception. A @racket[return] The return mechanism is based on @racket[call/cc]. A @racket[define/return]
raises that exception, and the definition forms catch it around the function form captures the current continuation of the function body and binds it to
body. The contracted form additionally checks early-returned values against the return identifier supplied by the programmer. Calling that identifier
the result contract. leaves the body and returns the supplied value as the result of the function.
No exception is raised and no exception handler is installed.
See @racketmodname[define-return/contract] for the contracted version of @section{Definitions with an early return}
this module.
@section{Return} @defform[(define/return def return body ...)]{
@defform[(return val)]{ Like @racket[define], but @racket[body] may call @racket[return] to leave
the definition early.
Returns @racket[val] from the nearest dynamically enclosing The @racket[return-id] identifier is part of the surface syntax. It is not a
@racket[define/return] or @racket[define/contract/return] body. predefined binding exported by this module. This keeps the form hygienic: the
programmer chooses the name of the local escape continuation, and the body
The form is not a general escape continuation. It raises an internal return uses that same name explicitly.
exception. When used outside a body installed by this library, that exception
escapes.
}
@section{Uncontracted definitions}
@defform[(define/return def body ...)]{
Like @racket[define], but @racket[body] may use @racket[return] to leave the
definition early.
@racketblock[ @racketblock[
(define/return (status->symbol code) (define/return (status->symbol code) return
(unless (number? code) (unless (number? code)
(return 'not-a-number)) (return 'not-a-number))
(when (= code 0) (return 'ok)) (when (= code 0) (return 'ok))
@@ -51,9 +41,7 @@ definition early.
(when (> code 5) (return 'out-of-range)) (when (> code 5) (return 'out-of-range))
(cond (cond
((= code 1) 'normal) ((= code 1) 'normal)
((>= code 2) (string->symbol (format "code-~a" (* code code)))) ((>= code 2) (string->symbol (format "code-~a" (* code code))))))
)
)
] ]
The final expression is used when no early return is taken. The final expression is used when no early return is taken.
@@ -69,24 +57,37 @@ The final expression is used when no early return is taken.
} }
@section{Contracted definitions} @section{FFI-style checks}
See @racketmodname[define-return/contract]. The form is deliberately small, but it fits the style of many C libraries.
For example, a wrapper can leave immediately when an FFI call returns a null
pointer:
@racketblock[
(define/return (make-codec-context codec-id) return
(define codec (avcodec-find-decoder codec-id))
(unless codec
(return 'decoder-not-found))
(define ctx (avcodec-alloc-context3 codec))
(unless ctx
(return 'alloc-codec-context-failed))
ctx)
]
The second call is evaluated only when the first one succeeded. If either
call returns @racket[#f], the function returns the corresponding symbol.
Otherwise the final expression returns the allocated context.
@section{Notes} @section{Notes}
The mechanism is intentionally small. It uses @racket[with-handlers] and an The mechanism is intentionally just a local escape. It uses @racket[call/cc]
internal exception type; it does not introduce prompts, continuations, or a to capture the continuation around the function body. The captured
new calling convention. continuation is bound to @racket[return] and can be called as a normal
procedure with one value.
For @racket[define/contract/return], the normal function contract remains the Since @racket[return] is a continuation, not a syntax form, it can also be
job of @racket[define/contract]. The extra returner only exists for the path passed to local helper procedures when that is useful. It should normally be
where @racket[return] leaves the body through an exception handler. That path used only as an escape from the dynamic extent of the function body.
would otherwise bypass the ordinary result position of the function body.
The contracted form does not try to parse arbitrary contract syntax. It
splits the inline contract form syntactically and reuses its last element as
the early-return result contract. This works well for ordinary result
contracts such as @racket[number?], @racket[symbol?],
@racket[(or/c symbol? number?)], and the result position of @racket[->*]
contracts.