202: Pattern-matching Variant of the and-let* Form that Supports Multiple Values

by Panicz Maciej Godek

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-202@nospamsrfi.schemers.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.

Abstract

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 abilities to pattern-match (or "destructurally bind") the values of evaluated expressions (where the match failure causes short-circuiting rather than raising an error), and to handle multiple values (where only the falsehood of the first value causes short-circuiting).

Issues

None at present.

Rationale

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 and 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 the 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 the function like

  h x = case (f x) of
          Just y -> case (g (k y)) of
                      Just z -> Just (l z)
                      Nothing -> Nothing
          Nothing -> Nothing
to 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 has been defined in the SRFI-2 document.

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))
             ((not (null? my-list))))
    (do-something my-list))

  (define (lookup key alist)
    (and-let* ((x (assq key alist)))
      (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 first example is not exactly equivalent, because it assumes that the test (not (null? my-list)) has the same meaning as (pair? my-list), which doesn't need to be true, even if usually is. Moreover, the body of the form contains an unnecessary cons operation which may incur performance penalty.)

Specification

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.

Implementation

(define-syntax and-let*
  (lambda (stx)
    (syntax-case stx ()

      ((_)
       #'#t)

      ((_ ())
       #'#t)

      ((_ () body ...)
       #'(let () body ...))

      ((_ ((value expression) rest ...) body ...)
       (identifier? #'value)
       #'(let ((value expression))
	   (and value
		(and-let* (rest ...)
		   body ...))))

      ((_ ((value expression) rest ...) body ...)
       #'(match expression
	   (value
	    (and-let* (rest ...)
	      body ...))
	   (_ #f)))

      ((_ ((condition) rest ...)
	  body ...)
       #'(and condition
	      (and-let* (rest ...)
		body ...)))

      ((_ ((value values ... expression) rest ...) body ...)
       (identifier? #'value)
       #'(call-with-values (lambda () expression)
	   (lambda args
             (match args
              ((value values ... . _)
	       (and value
		    (and-let* (rest ...)
	               body ...)))
	      (_ #f)))))

      ((_ ((values ... expression) rest ...) body ...)
       #'(call-with-values (lambda () expression)
          (lambda args
            (match args
	      ((values ... . _)
	       (and-let* (rest ...)
	          body ...))
	      (_ #f)))))
      )))

A working implementation for Guile Scheme can be found in the (grand syntax) module of the (grand scheme) glossary.

A working implementation for the Racket language can be found in the "grand-syntax.rkt" module of the Sracket framework.

Acknowledgements

??? credit where it is due

Copyright

Copyright © Panicz Maciej Godek (2020).

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