Files
define-return/scrbl/define-return.scrbl
T
2026-05-10 23:29:06 +02:00

179 lines
5.3 KiB
Racket

#lang scribble/manual
@(require (for-label racket/base
racket/contract
"../main.rkt"))
@title{define-return}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[define-return]
The @racketmodname[define-return] library provides definition forms with an
explicit early return. This 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 early return is implemented with an internal exception. A @racket[return]
raises that exception, and the definition forms catch it around the function
body. The contracted form additionally checks early-returned values against
the result contract.
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].
@section{Return}
@defform[(return val)]{
Returns @racket[val] from the nearest dynamically enclosing
@racket[define/return] or @racket[define/contract/return] body.
The form is not a general escape continuation. It raises an internal return
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[
(define/return (status->symbol code)
(when (= code 0) (return 'ok))
(when (< code 0) (return 'failed))
(when (> code 5) (return 'out-of-range))
(unless (number? code)
(return 'not-a-number))
(cond
((= code 1) 'normal)
((>= code 2) (string->symbol (format "code-~a" (* code code))))
)
)
]
The final expression is used when no early return is taken.
@codeblock|{
(status->symbol 0) ; => 'ok
(status->symbol -1) ; => 'failed
(status->symbol 10) ; => 'out-of-range
(status->symbol "Hi") ; => 'not-a-number
(status->symbol 1) ; => 'normal
(status->symbol 3) ; => 'code-9
}|
}
@section{Contracted definitions}
@defform*[
((define/contract/return (name . formals)
(contract-part ... result-contract)
body ...)
(define/contract/return name
result-contract
value))
]{
Like @racket[define/contract], but the body may use @racket[return].
The first form defines a contracted function. 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 the same result contract, so the result
contract is passed through Racket's contract machinery.
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)
(-> 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)
(-> symbol?)
(let ((cs (current-seconds)))
(when (= (remainder cs 3) 0) (return "deelbaar door 3"))
(when (= (remainder cs 2) 0) (return 'dividable-by-2))
'yes))
]
The result contract is @racket[symbol?]. Returning
@racket['dividable-by-2] or @racket['yes] is accepted. Returning the string
@racket["deelbaar door 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)
(->* () #:rest (listof number?) number?)
(when (null? xs) (return 0))
(apply + xs))
]
@codeblock|{
(sum) ; => 0
(sum 1 2) ; => 3
(sum 1 'x) ; contract violation
}|
The second form defines a contracted value. The value expression may use
@racket[return], and the returned value is checked against
@racket[result-contract].
@racketblock[
(define/contract/return v
number?
(return 'ss))
]
This definition raises a contract violation, because @racket['ss] does not
satisfy @racket[number?].
}
@section{Notes}
The mechanism is intentionally small. It uses @racket[with-handlers] and an
internal exception type; it does not introduce prompts, continuations, or a
new calling convention.
For @racket[define/contract/return], the normal function contract remains the
job of @racket[define/contract]. The extra returner only exists for the path
where @racket[return] leaves the body through an exception handler. That path
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.