273: Extensions to Data (Type-)Checking

by Artyom Bologov

Status

This SRFI is currently in draft status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-273@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Table of contents

  1. Abstract
  2. Rationale
  3. Synopsis
  4. Specification
    1. Return value checking: (lambda-checked args [=> predicate] body...), (case-lambda-checked ((args [=> predicate] ...) body ...) ...), and (define-checked (name args ...) [=> predicate] body ...)
    2. auxiliary syntax (check-impl? datum)
    3. (define-check name predicate)
    4. (declare-checked name predicate) | (declare-checked (name (arg pred) ...) [=> predicate])
    5. (srfi 273) (define-values-checked (vars ...) (checks ...) form)
    6. (check-of datum)
    7. (procedure-check-of proc)
    8. Optimizable check patterns / combinators
  5. Implementation
  6. Acknowledgements
  7. Copyright

Abstract

The original SRFI 253 established a basis for type-checked (or otherwise checked) data handling. But it lacked some quality-of-life features. This SRFI extends SRFI 253 to match existing implementation practice and common sense. Provided extensions are: check aliasing with define-check; pre- and post-declaration of type / check with declare-checked; return value checks in lambda-checked, case-lambda-checked, and define-checked; the check-of and procedure-check-of procedures to derive a suitable check for a datum / procedure; and some optimizable checking patterns suggested to implementors.

Issues

Rationale

SRFI 253 was good in establishing a fundamental set of data-checking primitives. It enabled portable strongly typed programming on multiple implementations. However, there was some incompleteness and mismatch with implementation practice in it. In particular:

This SRFI extends SRFI 253 based on additional exposure to type systems in languages like Common Lisp, TypeScript, and Haskell. It closes the gaps left by SRFI 253, two years after its publication. Hopefully, there won’t be a need for a third SRFI in this series.

Synopsis

This SRFI extends SRFI 253 to match the practice / behavior of many implementations, and to add quality-of-life features that are not present in implementations but are nonetheless a logical extension of SRFI 253 ethos. Thus it makes Scheme code with 253’s primitives even more optimizable, correct, and compliant. Backwards compatibility with SRFI 253 is an explicit priority, so all code written with SRFI 253 continues to work in implementations including this SRFI.

Included features (painfully missing from SRFI 253) are:

Specification

Return value(s) checking: (srfi 253) (lambda-checked args [=> (predicate ...)] body...), (case-lambda-checked (args [=> (predicate ...)] body ...) ...), and (define-checked (name args ...) [=> (predicate ...)] body ...)

SRFI 253 included a way to specify argument checks for procedures via lambda-checked and the like. However, what was missing was return-value checking. Most implementations supporting type annotations also support return-type checks. SRFI 253 not mentioning these and leaving return-value checking to values-checked form was an under-specification. Thus this addition.

Implementations supporting this SRFI must support return-value checking via a => operator in procedure-creating checking forms. lambda-checked, case-lambda-checked (whenever available), and define-checked procedure form, in particular. This operator should be followed by a parenthesized list of predicates checking the return value(s). (Note that parentheses are not optional, like in SRFI-253 value-checked). It is an error if any of these predicate checks returns false.

Use of => for this might be seen as an unnecessary overload of an already overused symbol. But this is the most minimal and recognizable choice of an operator. It is also similar in syntax to e.g. JavaScript arrow functions.

auxiliary syntax (check-impl? datum)

This form, when used in either of SRFI 253 or this SRFI primitives, passes the datum to the implementation unaltered. The form itself is discarded as a non-predicate. This is useful to allow implementation-specific types and types not covered by standard predicates.

check-impl? is not guaranteed to work in check-arg, because check-arg might be implemented as a procedure. check-impl? should be avoided in this case.

(values-checked ((check-impl? uint)) -1) ;; is an error
(define-checked (ref (data (check-impl? pointer)) (idx integer?))
  (implementation-specific-ffi-pointer-ref data idx))

(srfi 273) syntax (define-check name predicate)

This form defines a new name-d checking predicate based on the provided predicate (possibly evaluated). In the simplest implementation form, it might be a mere define aliasing a predicate value to name. In a more complex, type-based implementation, it may define a new type derived from predicate-matching type, whenever derivable.

Implementations — especially compiled and/or statically typed — may require define-check to happen in a separate module from its first use. This is to make static type inference easier. However, implementors must allow intra-module define-check, possibly falling back to dynamic predicate definition.

(define-check any? (constantly #t))
(define-check email? string?)
(define-check positive-integer? (conjoin integer? positive?))

(check-arg email? "srfi-273@example.com") ;; terminates normally
(check-arg positive-integer? -3) ;; is an error

(srfi 273) syntax (declare-checked name predicate) | (declare-checked (name args ...) [=> (predicate)])

This syntax declares checks for a given value (in the first form) before / after the value itself was bound to a name. Or argument / return checks for a procedure defined separately. In the case of the procedure check declaration, argument checks are specified as either (arg pred?) (list of 2 elements) or arg (a single symbol,) like in define-checked Return value type checks, introduced by => symbol, are syntactically optional. They specify the checks for procedure return value(s) when provided.

The return value is unspecified.

It is highly recommended that checked declaration be supported even for symbols from other libraries. This supports post-factum type hardening, similar to TypeScript type declaration files.

;; Declare checks for pre-existing standard functions
(declare-checked (identity x) => ((constantly #t)))

;; Declare the contract for a function before it is defined
(declare-checked (numbered-string (str string?) (idx integer?)) => (string?))
(define (numbered-string str idx)
  (string-append str (number->string idx)))

Previous version of SRFI document used declare-checked only listing types without argument names. Argument names were added because there’s a descriptive function to declare-checked. To make fellow programmer’s codebase exploration easier. Without the need to consult procedure definition in addition to declare-checked signature.

(srfi 273) syntax (define-values-checked (vars ...) (checks ...) form)

Same as define-values, but makes sure the values returned by form abide by checks. Further symbol value modifications may be checked too.

(srfi 273) procedure (check-of datum)

Returns a check the provided datum satisfies, as a quoted symbol or expression. Returns #f if the check cannot be derived. The returned value should be eval-uatable in the current (interaction-environment) to get the check procedure that the datum satisfies. The precision to which the check is derived is not specified.

Conceptually similar to type-of and typeof of other languages and type-fetching primitives of some Scheme implementations.

;; Any of these might return #f and still be compliant
(check-of 3)
;; => integer? or number? or exact? or exact-integer?
(define-checked x (cut = 3 <>) 3)
(check-of x)
;; => (cut = 3 <>) or number checks as above

(srfi 273) procedure (procedure-check-of proc)

Accepts a procedure (as per procedure?) Returns two values:

  1. List of checks for arguments of proc as expressions / symbols / #f (if argument check cannot be determined.) In case the procedure accepts rest arguments, the returned checks are only covering required arguments. Checking for rest / optional argument presence is beyond this SRFI.
  2. List of checks for return values of proc.

Either of lists can be #f.

Like in check-of, each expression in either list should be eval-uatable in the current (interaction-environment).

;; Example numbered-string function from before
(procedure-check-of numbered-string)
;; => (string? integer?), (string?)

(procedure-check-of +)
;; Rest argument not included
;; => (), (number?)

(procedure-check-of eval)
;; Rest argument is ignored, fetch it elsewhere
;; => ((constantly #t) (constantly #t)), ((constantly #t))

(procedure-check-of procedure-check-of)
;; (procedure?), ((disjoin boolean? symbol? pair?) (disjoin boolean? symbol? pair?))

Optimizable check patterns / combinators

While SRFI 253 established behaviors for primitive/built-in types, it didn’t specify something like union types of sequence types. This is an under-specification. The proposed solution is to optimize the most obvious patterns used for these. Say, as SRFI 235 combinators. Or SRFI 26 pseudo-currying.

While all of these are listed here as relying on existing SRFI primitives, code analysis that can prove the equivalence of the combinator used with these can safely optimize these patterns:

(lambda (x) (and (integer? x) (positive? x)))
;; === (conjoin integer? positive?)
(lambda (x) (not (integer? x)))
;; === (complement integer?)

Note that none of these are required for this SRFI compliance, and will work even in the absence of any special treatment from the implementation. These are combinators working with functions, after all.

(srfi 235) (disjoin pred ...)

Effectively a union check/type.

(srfi 235) (conjoin pred ...)

Effectively an intersection check/type.

(srfi 235) (complement pred)

Not check/type.

(srfi 26 + r7rs) (cut memv <> '(items ...)) or (cut memv <> (list items ...))

Member check/type against constants. memv is picked specifically, as it’s close to implementation-specific type systems, unlike more involved and non-optimizable member.

(srfi 26 + r7rs) (cut eqv? constant)

Single-member check/type.

(srfi 26 + srfi 1) (cut every pred <>)

List-of check.

(srfi 26 + srfi 43) (cut vector-every pred <>)

Vector-of check.

(srfi 235) (constantly #t) and (constantly #f)

Top/any check/type; and bottom/none check/type respectively.

Implementation

Find the sample implementation for return value checking in SRFI 253 repository. It is also contributed to most existing SRFI 253 implementations (Chicken, Chibi, Gauche, Kawa,), ETA unspecified.

As for (srfi 273) APIs, here’s the minimal implementation:

(define-syntax define-check
  (syntax-rules ()
    ((_ name predicate)
     (define name predicate))))
(define-syntax define-values-checked
  (syntax-rules ()
    ((_ (vars ...) (checks ...) form)
     (define-values (vars ...) form))))
(define (check-of datum)
  #f)
(define (procedure-check-of proc)
  (values #f #f))
(define-syntax declare-checked
  (syntax-rules (=>)
    ((_ name predicate)
     (when #f #f))
    ((_ (name . args))
     (when #f #f))
    ((_ (name . args) => return)
     (when #f #f))))

Acknowledgements

Thanks to Yukari Hafner for working on trivial-arguments Common Lisp library, where the author of this SRFI have contributed function type inspection, inspiring procedure-check-of.

Thanks also to Felix Winkelmann for working on CRUNCH compiler, during the use of which the need for type-checking macros manifested.

Thanks to John Cowan and Peter McGoron for the discussion on better procedure-check-of and check-of (previously derive-check) design.

© 2026 Artyom Bologov.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Arthur A. Gleckler