by Panicz Maciej Godek
This SRFI is currently in final status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to srfi-202@nospamsrfi.schemers.org
. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.
The SRFI-2 library introduced the and-let*
form
for short-circuited evaluation in the style of the and
form, with the ability to capture the (non-#f
) results
in the style of the let*
form. This document extends
the and-let*
form with the ability to pattern-match (or
"destructurally bind") the values of evaluated expressions (where
the match failure causes short-circuiting rather than raising an
error) and the ability to handle multiple values (where only the
falsehood of the first value causes short-circuiting).
It is well known that, in the context of the if
condition,
Scheme treats #f
as a false value, and every other value
is considered true (or "true-ish").
This design decision has led to the development of the idiom in which the
#f
is considered to represent "the absence of value" or a "failure
of computation", whereas any other value is considered to represent a meaningful
result.
In strongly typed languages, this pattern is usually expressed in terms of algebraic
data types. For example, the Haskell language defines the polymorphic type
Maybe a
as either Just a
or Nothing
,
where Just a
is used to represent a value of type a
,
and Nothing
is used to represent the absence of value.
Moreover, in many functional languages, the types isomorphic to Maybe
are considered to be "monads", which means that they can use some facilities
that those languages provide for sequencing functions that return things that
are considered to be "monads". In particular, Haskell provides a special syntactic
form called the do
notation, which allows a function like
h x = case (f x) of Just y -> case (g (k y)) of Just z -> Just (l z) Nothing -> Nothing Nothing -> Nothingto be rewritten as
h x = do y <- (f x) z <- (g (k y)) return (l z)
where f
and g
(and h
) return
optional values, and k
and l
return obligatory
values.
Since these facilities rely on some advanced features of type systems
that are lacking from most Scheme implementations, Schemers have invented
a special form specifically for dealing with optional values. The form
is called and-let*
, and was defined in SRFI 2.
Accordingly, the equivalent code using Scheme can be written as
(define (h x) (and-let* ((y (f x)) (z (g (k y)))) (l z)))
However, the original definition of and-let*
does not allow
one to handle multiple values. It also doesn't allow one to destructure the value
of the evaluated expression. The purpose of this SRFI is to extend the
and-let*
form with these capabilities.
Therefore, with the capabilities from this SRFI, the two first examples from the Rationale section of SRFI 2, namely:
(and-let* ((my-list (compute-list)) ((pair? my-list))) (do-something my-list)) (define (lookup key alist) (and-let* ((x (assq key alist)) ((pair? x))) (cdr x)))
could roughly be rewritten as:
(and-let* ((`(,my . ,list) (compute-list))) (do-something `(,my . ,list))) (define (lookup key alist) (and-let* ((`(,key . ,value) (assq key alist))) key))
The extensions provided here do not invalidate or modify any of the prior
uses of the SRFI 2's and-let*
form.
In particular, the and-let*
form allowed two types of expressions
inside of its "claws":
(variable expression)
and
(guard-expression)
This SRFI document allows variable
to be an arbitrary
pattern that is a valid pattern of the underlying pattern matcher
(see SRFI 200
Pattern Matching for more details):
(pattern expression)
In particular, since patterns can be literal values, it is possible to negate the condition:
(and-let* ((my-list (compute-list)) (#f (null? my-list))) (do-something my-list))
If the value of an expression does not match the pattern, then the
value of the whole and-let*
form is #f
.
This variant of the and-let*
form also supports multiple
values, each of which can be a pattern:
(pattern patterns ... expression)
If the first pattern
is a variable, then it must have
a non-#f
value in order to proceed with evaluation
— otherwise, the value of the whole and-let*
form
becomes #f
.
If the expression
returns fewer values than expected,
then the value of the whole and-let*
expression becomes
#f
. If it returns more values than expected, then the
additional values are ignored.
In addition, following SRFI 71 and SRFI 201 - the
extension proposed here supports the values
keyword specially, allowing one to capture "the remaining
values" in a list - for example, the claws of form
((values p1 p2 . v*) expression)
(where values
is to be treated as a
literal symbol) requires that
expression
returns at least two values;
the first two will be bound to patterns p1
and p2
(if they match), and the
remaining values, if present, will be captured
in a list v*
.
If the expression
returns (in this case)
less than two values, then the whole
and-let*
form short-circuits to
#f
.
(define-syntax and-let* (lambda (stx) (syntax-case stx (values) ((_) #'#t) ((_ ()) #'#t) ((_ () body ...) #'(let () body ...)) ((_ ((name binding) rest ...) body ...) (identifier? #'name) #'(let ((name binding)) (and name (and-let* (rest ...) body ...)))) ((_ (((values . structure) expression) rest ...) body ...) #'(call-with-values (lambda () expression) (lambda args (match args (structure (and-let* (rest ...) body ...)) (_ #f))))) ((_ ((value expression) rest ...) body ...) #'(match expression (value (and-let* (rest ...) body ...)) (_ #f))) ((_ ((condition) rest ...) body ...) #'(and condition (and-let* (rest ...) body ...))) ((_ ((value * ... expression) rest ...) body ...) (identifier? #'value) #'(call-with-values (lambda () expression) (lambda args (match args ((value * ... . _) (and value (and-let* (rest ...) body ...))) (_ #f))))) ((_ ((value ... expression) rest ...) body ...) #'(call-with-values (lambda () expression) (lambda args (match args ((value ... . _) (and-let* (rest ...) body ...)) (_ #f))))) )))
The repository for this SRFI contains an implementation
for Guile built on top of the
(ice-9 match)
module a.k.a.
Wright-Cartwright-Shinn matcher, and an implementation
for Racket, built on top of Racket's
racket/match
module. A suite of
testable examples is available for both
Guile and Racket.
The original and-let*
form was conceived
by Oleg Kiselyov, who owes the credit for both the
idea and the name. It was one of the earliest documents
described within the SRFI process, as it received
the number 2.
The editor of this SRFI turned out to be a real Schemer, and when I asked him if it would be possible to make sure that it gets the number 202, he said he would do his best. (I'm sorry for turning you in, Arthur. [ed. note: I confess that this wasn't the first numerical reservation for a SRFI.])
This SRFI owes the support for values
keyword to Marc Nieper Wißkirchen, who pointed out
the limitations of the solution that
I initially proposed. Marc's remarks about backward
compatibility also helped to improve this text.
The technique of pattern matching on data structures was brought to Scheme by Andrew K. Wright and Robert Cartwright through Alex Shinn's implementation, which has been distributed with many implementations of Scheme.
My first contact with Scheme was through Guile, which is a part of the GNU project. I found Guile to have exceptionally good documentation, optimized for people who spend their lives in VT100 terminal emulators. The maintainers of Guile at that time were Neil Jerram, Ludovic Courtes, Andy Wingo and Mark Weaver.
The person responsible for convincing me that it makes sense to express my ideas and developments regarding Scheme in the form of SRFI documents is John Cowan.
© 2020 Panicz Maciej Godek.
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.