define-typedby Arne Babenhauserheide
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-284@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.
This SRFI defines the forms define-typed and define-typed* for creating procedures with checked type boundaries. It allows checking the arguments and/or the return value or values with simple predicate procedures and raises exceptions on usage when they don’t match. The form define-typed* supports keyword and optional arguments. The implementation for Guile is optimized to allow the compiler to use the predicates as guarantees to create more optimized code and provides type information for runtime introspection that supports SRFI 283.
It’s unclear whether lambda-typed should be provided. With the simpler definition here, it is possible, but it would not support the properties and would be a hassle to add to the implementation. If you don’t want to name the procedure, adding types may not be useful. However it could be useful to add state inside the procedure, e.g. via (define name (let ((counter 0)) (lambda (x) (set! counter (1+ counter)) counter)))
Type boundaries make it easier to learn new APIs (Stefik et. al 2014) and can enable more code validation at editing-time. This proposal is a simpler version of SRFI 253 (though its implementation predates SRFI 253), providing only procedure-level checks. It works well together with the checks for data defined in SRFI 253 and SRFI 273.
Different from define-checked in SRFI 253, define-typed does not interleave types with arguments, but provides them as second list after the arguments. This keeps the procedure arguments easier to grasp at a glance as long as they all fit on a single line, but becomes harder to understand when the procedure has more than a handful of arguments. Also the return type is defined as part of the procedure header to keep all type information in one place. But define-checked is clearly the better name.
The argument types and the return type are separated by ->. This symbol was chosen because it is commonly used to give the return type in gradual typing systems like the ones in Python and Typescript. The implementation of define-typed actually started with purely structural type definition spearheaded by sph-sc (with the return type first, in the position of the procedure name), but that turned out to cause more errors due to misinterpreting return-types as argument types or the other way round.
The form define-typed* supports named and optional arguments, while define-typed only supports positional arguments, because supporting named and optional arguments incurs a cost in runtime and implementation complexity.
Different from SRFI 253, this SRFI only provides two forms and no helpers, to minimize learning time. It is implemented purely in syntax-rules to improve portability, but the runtime introspection of the implementation (using procedure-properties) will require porting to be usable in other implementations, which is why it’s unspecified here.
To avoid conceptual and implementation complexity, there is no support for procedures with variable numbers of arguments numbers or rest arguments.
The implementation for Guile is as fast as manually added type guards at the start of the body as described in the article Optimizing Guile Scheme. This makes define-typed an easier way to speed up hot inner loops, as long as the predicates give the compiler the needed information.
The form define-typed accepts an additional form after the arguments. This form contains predicates to check the arguments and/or the return value.
To add argument and return type information the procedure
(define (foo a)
(number->string a))
a second type form is added after (foo a)
(define-typed (foo a) (number? -> string?)
(number->string a))
This second form holds predicate procedures. The predicates before the -> are the checks for the arguments, the predicate after the -> is the check for the return value.
define-typedFormal structure: (define-typed name-and-arguments types body).
There are three ways to call define-typed: only argument types, only return type, and argument-types and return types. Each type can be a predicate-procedure, a lambda form, or #f (for “no type check”).
The return types can be either a single predicate for a single value, for example string?, a form with multiple predicates for multiple values, one per value, for example (string? string?), or a form with a single predicate that receives all return values as list, for example (all-string?).
The following examples are all valid
;; only type the argument
(define-typed (foo a) (integer?)
(number->string a))
;; only type the return value
(define-typed (foo a) (-> string?)
(number->string a))
;; type argument and return value
(define-typed (foo a) (integer? -> string?)
(number->string a))
;; type the argument and two return values
(define-typed (foo a) (integer? -> (string? integer?))
(values (number->string a) a))
;; type the argument and use a single predicate for all return values
(define (all-integer? val) (not (member #f (map integer? val))))
(define-typed (foo a) (integer? -> (all-integer?))
(values a a))
Procedures defined by define-typed shall call the predicate checks of their respective arguments or types before entering the procedure body must and raise errors if one or more predicates return #f. If return value predicates are defined, they shall run before the values are returned and likewise raise errors.
If the implementation supports docstrings or properties, for example by using literal types after the procedure header, then they should be used after the type-header. Example from Guile with implementation-specific behavior:
(define-typed (foo a) (integer? -> string?) #((prop val)) "docstring" "return value")
(foo 5) ;; => "return value"
(procedure-documentation foo) ;; => "docstring"
(procedure-properties foo)
;; => ((argument-types #<procedure integer? (_)>)
;; (return-type . #<procedure string? (_)>)
;; (name . foo)
;; (documentation . "docstring")
;; (prop val))
define-typed*The form define-typed* works like the form define-typed, but it supports named and optional arguments in the style provided by the implementation. The named and keyword arguments are typed in the way in which they are defined.
Example for Guile:
(define-typed* (foo x #:key bar) (float?? #:key float? -> float?)
x)
The sample implementation in define-typed.scm is far more complex than required because it grew out of the structural type support, but it implements all features described here for Guile Scheme, and its performance matches that of hand-typed code. For the simple arithmetic procedure in the benchmark, the typed version reaches 3x the performance of the untyped procedure. To check support and performance, use the test file benchmark.scm.
That this implementation became a SRFI is thanks to the work of Artyom Bologov in SRFI 253 and SRFI 283. That it exists at all is thanks to both the work of sph on sph-sc and c-indent for the initial form of define-typed and the article Optimizing Guile Scheme by David Thompson. Without these, I would likely never have started on the path that led here.
© 2026 Arne Babenhauserheide.
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.