235: Combinators

by John Cowan (text), Arvydas Silanskas (code)

Status

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

Abstract

This SRFI contains various procedures that accept and return procedures, as well as a few others, drawn from an earlier version of Chicken. Common Lisp has a few of them too, and more come from the Standard Prelude from Programming Praxis. Using these procedures helps to keep code terse and reduce the need for ad hoc lambdas.

Rationale

Many procedures such as map, filter, fold, and their equivalents for data structures other than lists accept an argument specifying the behavior to be performed on each element of the data structure. This can be done in one of two ways:

Those composition procedures, called combinators, have been identified by the Scheme and Common Lisp communities as reducing such fragmentation while keeping the code dense.

Notation

Many of these procedures are documented in the style of SRFI 219. Rather than showing only how the procedures themselves are invoked, it also shows how the returned procedures would be invoked. This is done in order to make the descriptions easier to understand. For example, if complement were documented in the standard style, the description would say "Returns a procedure which, when applied to an argument, returns #t when proc would return #f when applied to the same argument, and #f otherwise", which is more convoluted and harder to understand. However, this is merely a documentation style; it would be pointless to actually invoke these procedures in this fashion.

Specification

((constantly obj ...) arg ...)

Returns the objs as its values, ignoring args.

(map (constantly 3) '(1 2 3)) => (3 3 3)

((complement proc) obj)

Returns #t when (proc obj) returns #f, and #f otherwise.

(map (complement (lambda (x) #f)) '(1 2 3)) => (#t #t #t)
(map (complement (lambda (x) 3)) '(1 2 3)) => (#f #f #f)

((flip proc) . objs)

Returns what (apply proc (reverse objs ) returns.

((flip list) 1 2 3) => (3 2 1)

((swap proc) obj₁ obj₂ . objs)

Returns (apply proc obj₂ obj₁ objs).

((swap cons) 1 2) => (2 . 1)

((on-left proc) obj₁ obj₂)

Returns (proc obj₁).

(map (on-left list) '(1 2 3) '(4 5 6)) => '((1) (2) (3))

((on-right proc) obj₁ obj₂)

Returns (proc obj₂).

(map (on-right list) '(1 2 3) '(4 5 6)) => '((4) (5) (6))

((conjoin predicate ...) arg ...)

The predicates are applied in turn to the args as follows: If a call to a predicate returns false, no more predicates are applied and #f is returned. If all predicates return true, then the last value is returned. If there are no predicates, #t is returned.
(map (conjoin even? exact?) '(1.0 1 2.0 2)) => '(#f #f #f #t)

((disjoin predicate ...) arg ...)

The predicates are applied in turn to the args as follows: If a call to a predicate returns true, no more predicates are applied and its value is returned. If all predicates return false, then the last value is returned. If there are no predicates, #f is returned.
(map (disjoin even? exact?) '(1.0 1 2.0 2)) => '(#f #t #t #t)

((each-of proc ... ) arg ...)

Applies each of the procs in turn to args, discarding the results and returning an unspecified value.

(define (print-sum . numbers)
  (display
    (string-join (map number->string numbers)
                 "+" 'infix))
  (newline))

(define (print-product . numbers)
  (display
    (string-join (map number->string numbers)
                 "*" 'infix))
  (newline))

((each-of print-sum print-product) 1 2 3) => undefined ;prints:
1+2+3
1*2*3

((all-of predicate) list)

Applies predicate to each element of list in turn, and immediately returns #f if predicate is not satisfied by that element. If every element satisfies predicate, returns the result of the last call to predicate. If list is empty, returns #t.

((all-of even?) '(2 4 6)) => #t
((all-of odd?) '(1 2 3)) => #f

((any-of predicate) list)

Applies predicate to each element of list in turn, and if predicate is satisfied by that element, immediately returns the result of calling predicate. If no element satisfies predicate returns #f. If list is empty, returns #f.

((any-of even?) '(2 4 5)) => #t
((any-of odd?) '(2 4 6)) => #f

((on reducer mapper) obj₁ obj ...)

Applies mapper to each obj in any order and then applies reducer to all of the results in left to right order.

((on + -) 1 2 3) => -6

((left-section proc arg ...) obj ...)

Applies proc to args concatenated with objs.

(map (left-section - 1) '(1 2 3)) => (0 -1 -2)

((right-section proc arg ...) obj ...)

Applies proc to objs concatenated with the value of (reverse args).

(map (right-section - 1) '(1 2 3)) => (0 1 2)

((apply-chain proc₁ proc ...) arg ...)`

Applies the last proc to args returning zero or more values, then applies the previous proc to the values, returning more values, until the first proc has been invoked; its values are returned. For example, (apply-chain car cdr) returns a procedure that behaves like cadr:

(map (apply-chain car cdr) '((1 2 3) (4 5 6))) => (2 5)

((arguments-drop proc n) arg ...)
((arguments-drop-right proc n) arg ...)
((arguments-take proc n) arg ...)
((arguments-take-right proc n) arg ...)

Apply proc to the args after taking/dropping n arguments from args.

(apply (arguments-drop + 2) '(1 2 3 4 5)) => 12
(apply (arguments-drop-right + 2) '(1 2 3 4 5)) => 6
(apply (arguments-take + 2) '(1 2 3 4 5)) => 3
(apply (arguments-take-right + 2) '(1 2 3 4 5)) => 9

((group-by key-proc [=]) list)

Takes the elements of list and applies key-proc to each of them to get their keys. Elements whose keys are the same (in the sense of =, which defaults to equal?) are grouped into newly allocated lists, and a list of these lists is returned. Within each list, the elements appear in the same order as they do in list; in addition, the first elements of each list also appear in the same order as they do in list. If list is the empty list, it is returned.

((group-by car) '((1 2 3) (2 2 3) (1 4 5))) =>
  (((1 2 3) (1 4 5))
   ((2 2 3))

Syntax-like procedures

These are Scheme procedures that correspond to basic syntax. As usual in Lisps, thunk means a procedure that does not require arguments.

The following procedures are understood to be defined in this section:

  (define (a) (display #\a))
  (define (b) (display #\b))
  (define (c) (display #\c))
  (define (z) (display #\z))

(begin-procedure thunk ...)

Invokes thunks in order, and returns what the last thunk returns, or an unspecified value if there are no thunks.

(begin-procedure a b c) => unspecified (displays "abc")

(if-procedure value then-thunk else-thunk)

If value is true, invokes then-thunk and returns what it returns. Otherwise, invokes else-thunk and returns what it returns.

(if-procedure #t (lambda () 1) (lambda () 2)) => 1
(if-procedure #f (lambda () 1) (lambda () 2)) => 2

(when-procedure value thunk ...)
(unless-procedure value thunk ...)

If value is false/true, immediately returns. Otherwise, invokes each thunk in turn and then returns. In all cases an unspecified value is returned.

(when-procedure #t a b c) => unspecified ;prints "abc"
(when-procedure #f a b c) => unspecified ;prints nothing
(unless-procedure #t a b c) => unspecified ;prints nothing
(unless-procedure #f a b c) => unspecified ;prints "abc"

(value-procedure value then-proc else-thunk)

If value is true, invokes then-proc on it and returns what then-proc returns. Otherwise, invokes else-thunk and returns what it returns.

(value-procedure 2 (lambda (x) (+ x 1)) (lambda () 0)) => 3)
(value-procedure #f (lambda (x) (+ x 1)) (lambda () 0)) => 0)

(case-procedure value thunk-alist [ else-thunk ])

Searches thunk-alist for value (as if by assv). If there is a matching entry in thunk-alist, its cdr is invoked as a thunk, and case-procedure returns what the thunk returns. If there is no such entry in thunk-alist, invokes else-thunk (if present) and returns what it returns. If else-thunk is not present, the result is undefined.

(case-procedure 0 `((1 . ,a) (2 . ,z) (3 . ,c)) z) => undefined ;displays "z"
(case-procedure 0 `((1 . ,a) (2 . ,z) (3 . ,c))) => undefined ;displays nothing
(case-procedure 2 `((1 . ,a) (2 . ,b) (3 . ,c))) => undefined ;displays "b"

(and-procedure thunk ...)

The thunks are invoked from left to right, and if any thunk returns false, then #f is returned. Any remaining thunks are not invoked. If all the thunks return true values, the values of the last thunk are returned. If there are no thunks, then #t is returned.

(and-procedure
  (lambda () #t)
  (lambda () #f)
  (lambda () (error "fail"))) => #f

(eager-and-procedure thunk ...)

All the thunks are invoked from left to right. If any thunk returns false, then #f is returned. If all the thunks return true values, the value of the last thunk is returned. If there are no thunks, then #t is returned.

(eager-and-procedure
  (lambda () #t)
  (lambda () #f)
  (lambda () (error "fail"))) => error

(or-procedure thunk ...)

The thunks are invoked from left to right, and the first true value is returned. Any remaining thunks are not invoked. If all thunks return #f or if there are no thunks, then #f is returned.

(or-procedure
  (lambda () #t)
  (lambda () #f)
  (lambda () (error "fail"))) => #t

(eager-or-procedure thunk ...)

All the thunks are invoked from left to right, and then the first true value is returned. If all thunks return #f or if there are no thunks, then #f is returned.

(eager-or-procedure
  (lambda () #t)
  (lambda () #f)
  (lambda () (error "fail"))) => error

(funcall-procedure thunk)

Invokes thunk once, and returns what it returns. Note that the name funcall is derived from Common Lisp.

(loop-procedure thunk)

Invokes thunk repeatedly. Does not return unless via call/cc.

(loop-procedure (lambda () (a) (b))) => ; #displays "abab..." forever

(while-procedure thunk)

Invokes thunk repeatedly until it returns false. Returns an unspecified value.

(let ((i 0))
    (while-procedure
      (lambda ()
	(display i)
	(set! i (+ i 1))
	(< i 10)))) => unspecified ;prints "0123456789"

(until-procedure thunk)

Invokes thunk repeatedly until it returns true. Returns an unspecified value.

(let ((i 10))
  (until-procedure
    (lambda ()
      (set! i (- i 1))
      (display i)
       (> i 0)))) => unspecified ;prints "9876543210"

Other procedures

(always obj ...)

Ignores its arguments and always returns #t.

(always 1 2 3) => #t

(never obj ...)

Ignores its arguments and always returns #f.

(never 1 2 3) => #f

(boolean obj)

If obj is true, returns #t; otherwise returns #f.

(boolean 3) => #t
(boolean #f) => #f

Implementation

The sample implementation is in the SRFI 235 repository.

Acknowledgements

Thanks to Kon Lovett, author of the Chicken egg (who credits Graham Fawcett and Philip L. Bewig); to the anonymous author of the Programming Praxis web site; and to the members of the SRFI-235 mailing list.

© 2022 John Cowan, Arvydas Silanskas.

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