chickadee » srfi-189

SRFI-189: Maybe and Either

This SRFI defines two disjoint immutable container types known as Maybe and Either, both of which can contain objects collectively known as their payload. A Maybe object is either a Just object or the unique object Nothing (which has no payload); an Either object is either a Right object or a Left object. Maybe represents the concept of optional values; Either represents the concept of values which are either correct (Right) or errors (Left).

Note that the terms Maybe, Just, Nothing, Either, Right, and Left are capitalized in this SRFI so as not to be confused with their ordinary use as English words. Thus "returns Nothing" means "returns the unique Nothing object"; "returns nothing" could be interpreted as "returns no values" or "returns an unspecified value".

SRFI Description

This page includes excerpts from the SRFI document, but is primarily intended to document the forms exported by this egg. For a full description of the SRFI, see the full SRFI document.

Authors

John Cowan (text) and Wolfgang Corcoran-Mathe (sample implementation)

Specification

The four types Just, Nothing, Left, and Right are disjoint from each other and from all other Scheme types.

We speak of unwrapping a container when we extract its payload, and wrapping values in a container when we create the container with the values as its payload.

The following names are used for the arguments:

obj, defaultAny Scheme object.
maybeA Maybe object.
eitherAn Either object.
failureA procedure that accepts zero arguments (unless specified otherwise).
successA procedure that accepts one or more arguments.
predA predicate that accepts zero or more arguments.
equalA predicate that accepts two arguments and returns a boolean value. It is not necessarily an equivalence relation.
procA procedure that accepts zero or more arguments and returns zero or more values.
mprocA procedure that accepts zero or more arguments and returns zero or more values wrapped in a single Maybe or Either container.
listA Scheme list.
producerA procedure that accepts no arguments and returns some number of values.

If the procedures are passed arguments that do not have the type implied by the argument names, then a condition of kind (exn type assertion) is raised.

Maybe and Either

Constructors

just obj ...procedure

Monadic pure. Returns the objs wrapped in a Just.

nothingprocedure

Returns the unique Nothing object.

right obj ...procedure

Monadic pure. Returns the objs wrapped in a Right.

left obj ...procedure

Returns the objs wrapped in a Left.

list->just listprocedure
list->left listprocedure
list->right listprocedure

Returns a Just / Left / Right whose values are the elements of list. Note that these procedures are not monadic; they are equivalent to just/left/right but accept a list rather than multiple arguments. There is no need for list->nothing distinct from nothing.

maybe->either maybe obj ...procedure

If maybe is a Just, returns a Right with the same payload objects in the sense of eqv?; otherwise returns a Left of objs.

either->maybe eitherprocedure

If either is a Right, returns a Just with the same payload objects in the sense of eqv?; otherwise returns Nothing.

either-swap eitherprocedure

If either is a Left, return a Right with the same payload objects (in the sense of eqv?), and vice versa.

Predicates

just? objprocedure
nothing? objprocedure
right? objprocedure
left? objprocedure

Returns #t if obj is a Just, Nothing, Left, or Right respectively, and #f otherwise.

maybe? objprocedure

Returns #t if obj is a Maybe (that is, either a Just or Nothing) and #f otherwise.

either? objprocedure

Returns #t if obj is an Either (that is, either a Right or a Left) and #f otherwise.

maybe= equal maybe1 maybe2 ...procedure

Returns #t if all maybes are Nothing, or if they all are Justs and their respective payload objects are the same in the sense of equal, and #f otherwise.

either= equal either1 either2 ...procedure

Returns #t if all eithers are all Lefts or all Rights and their respective payload objects are the same in the sense of equal, and #f otherwise.

Accessors

maybe-ref maybe failure #!optional successprocedure

If maybe is a Just, tail-calls the procedure success on the values of its payload and returns the result. Otherwise, it tail-calls the procedure failure on no arguments and returns the result. The default value of success is values.

either-ref either failure #!optional successprocedure

If either is a Right, tail-calls the procedure success on the values of its payload and returns the result. Otherwise, it tail-calls the procedure failure on the values of its payload and returns the result. The default value of success is values.

maybe-ref/default maybe default ...procedure
either-ref/default either default ...procedure

If maybe/either is a Just/Right, returns the values of its payload; otherwise returns the defaults as multiple values.

Join and bind

maybe-join maybeprocedure
either-join eitherprocedure

Monadic join. If maybe/either is a Just/Right whose single payload object is a Maybe/Either, returns that payload; if it is a Nothing/Left, returns maybe/either. In all other cases (including a Just/Right with multiple values) it is an error; specifically, a condition of kind (exn type payload) is raised. Thus (maybe-join (just (just x)) returns Just x and (maybe-join (just (nothing)) returns Nothing, and similarly for either-join.

maybe-compose mproc1 mproc2 ...procedure
either-compose mproc1 mproc2 ...procedure

Returns an mproc that accepts zero or more arguments and applies the following iterative algorithm to each mproc:

The mproc is applied to the arguments and returns a Maybe/Either interim result. If the result is Nothing / a Left, it is returned as the final result. If it is a Just/Right, the next mproc is applied to the values of its payload, returning the next interim result. When no more mprocs are available, the interim result is returned as the final result.

It is an error if one of the mprocs does not accept as arguments the number of values wrapped in the Just/Right produced by its predecessor.

maybe-bind maybe mproc1 mproc2 ...procedure
either-bind either mproc1 mproc2 ...procedure

Monadic bind. If maybe/either is a Just/Right, it behaves as if maybe-compose/either-compose were applied to mprocs, and the resulting mproc applied to the payload of maybe/either. But if maybe/either is a Nothing/Left, it is immediately returned.

Sequence operations

These procedures treat Maybes (and in some cases Eithers) as sequences of length 0 or 1. This length depends only on the type of the container and not on the number of objects it contains.

maybe-length maybeprocedure
either-length eitherprocedure

Return 1 if maybe/either is a Just/Right, and 0 otherwise.

maybe-filter pred maybeprocedure
maybe-remove pred maybeprocedure
either-filter pred either obj ...procedure
either-remove pred either obj ...procedure

If maybe/either is a Just/Right and the values of its payload satisfy / do not satisfy pred, then return maybe; otherwise, returns Nothing / a Left of objs.

maybe-sequence mappable map #!optional aggregatorprocedure
either-sequence mappable map #!optional aggregatorprocedure

The purpose of these procedures is to transform a collection of Maybes/Eithers of some particular type into a Maybe/Either of a collection, often of the same type.

The mappable argument is the collection to be transformed, and the map argument is the appropriate procedure that can transform it, one element at a time. The signature of map is (map proc mappable), where proc is supplied by the implementation. Typical mappables are lists and vectors, and map and vector-map are useful map functions, though not the only possible ones.

Each element is processed as follows: If it is a Just/Right, its values are unwrapped and the aggregator function (which defaults to list) is applied to them. But if it is a Left/Nothing, that object is immediately returned as the value of maybe-sequence/either-sequence.

Assuming there are no Lefts/Nothings, the map function is then responsible for constructing the new mappable out of the results of the calls on aggregator.

For example, a list of Justs becomes a Just list of lists if the map procedure is map and the aggregator is list. In the same way, a vector of Rights containing one value each becomes a Right vector if the map procedure is vector-map and the aggregator is (lambda (x) x).

Protocol Conversion

The purpose of Maybe and Either is to provide first-class objects representing the distinction between success and failure in such a way that any number of success values (and, in the case of Either, any number of failure values) can be packaged up in a single object without reserving any values to indicate success or failure. A variety of protocols are already in use in Scheme and other Lisps to more or less perfectly represent the distinction. Some reserve a value to represent failure which cannot be returned as a success value; some handle only one success value; some are not first-class.

In order to facilitate communication between code using Maybe/Either and code using another protocol, the following procedures convert between Maybe/Either objects and some of the most usual protocols. Conversion in either direction is frequently lossy.

☞ The following procedures interface between the Maybe protocol and the list protocol, which uses an empty list to represent failure and a non-empty list to represent success.

maybe->list maybeprocedure
either->list eitherprocedure

Returns a list whose elements are the payload of maybe/either; if maybe/either is Nothing/Left, returns the empty list. It is an error to mutate the result of this procedure, though this implementation can't enforce this requirement.

list->maybe listprocedure
list->either list obj ...procedure

If list is empty, returns Nothing / wraps objs in a Left and returns it; otherwise, wraps the list elements in a Just/Right and returns that.

☞ The following procedures interface between the Maybe and Either protocols and the truth protocol, which uses #f to represent failure and a single true value to represent success.

maybe->truth maybeprocedure
either->truth eitherprocedure

If maybe/either is a Just/Right, returns its payload; otherwise returns #f. If the Just/Right does not have exactly one value, a condition of kind (exn type payload) is raised.

truth->maybe objprocedure
truth->either obj fail-obj ...procedure

If obj is #f, return Nothing / a Left of fail-objs; otherwise, return a Just/Right whose payload is obj.

☞ The following procedures interface between the Maybe protocol and the list-truth protocol, which uses #f to represent failure and a list to represent success.

maybe->list-truth maybeprocedure
either->list-truth eitherprocedure

If maybe/either is a Just/Right, returns a list whose elements are the payload; otherwise, returns #f. It is an error to mutate the result of this procedure, though this implementation can't enforce this requirement.

list-truth->maybe list-or-falseprocedure
list-truth->either list-or-false obj ...procedure

If list-or-false is #f, returns Nothing / a Left of objs; otherwise, wraps the list elements in a Just/Right and returns it.

☞ The following procedures interface between the Maybe and Either protocols and the generation protocol, which uses an end-of-file object to represent failure and any other value to represent success.

maybe->generation maybeprocedure
either->generation eitherprocedure

If maybe/either is a Just/Right, then its payload is returned. If the Just/Right does not have exactly one value, then a condition of kind (exn type payload) is raised. If maybe/either is Nothing / a Left, then an end-of-file object is returned.

generation->maybe objprocedure
generation->either obj fail-objs ...procedure

If obj is an end-of-file object, return Nothing / a Left of fail-objs. Otherwise, return obj wrapped in a Just/Right.

☞ The following procedures interface between the Maybe and Either protocols and the values protocol, which returns one or more values to represent success and zero values to represent failure.

maybe->values maybeprocedure
either->values eitherprocedure

If maybe/either is a Just/Right, returns the values of its payload; otherwise returns no values.

values->maybe producerprocedure
values->either producer fail-obj ...procedure

These procedures invoke producer with no arguments. If no values are returned, Nothing / Left of fail-objs is returned. Otherwise the values are wrapped in a Just/Right and returned.

☞ The following procedures interface between the Maybe protocol and the two-values protocol, which returns #f and #f to represent failure and a single value and #t to represent success. (This protocol is more often used in Common Lisp, where additional values are automatically discarded if the continuation expects only one.)

maybe->two-values maybeprocedure

If maybe is a Just, returns the value of its payload and the additional value #t; otherwise returns two values, both #f. If maybe does not have exactly one value, then a condition of kind (exn type payload) is raised.

two-values->maybe producerprocedure

This procedure is the inverse of maybe->two-values. It invokes producer with no arguments. If the second value is true, the first value is wrapped in a Just and returned; otherwise, Nothing is returned.

exception->either pred thunkprocedure

This procedure is in a sense the inverse of maybe-ref. A call to thunk is made, wrapped in an exception handler that examines any exception signaled during thunk's execution. If the condition object satisfies pred, it is wrapped in a Left which is then returned; if it does not, the exception is reraised. But if no exception is raised, the values returned by thunk are wrapped in a Right and the Right is returned.

Map, fold and unfold

maybe-map proc maybeprocedure
either-map proc eitherprocedure

Monadic map. If maybe/either is a Just/Right, applies proc to the payload values and wraps the returned values as a Just/Right; otherwise returns maybe/either.

maybe-for-each proc maybeprocedure
either-for-each proc eitherprocedure

If maybe/either is a Just/Right, applies proc to the payload values and discards the result; otherwise does nothing. Returns an unspecified result.

maybe-fold kons nil maybeprocedure
either-fold kons nil eitherprocedure

If maybe/either is a Just/Right, kons is invoked on the values of its payload and the additional value nil and the result is returned; otherwise, nil is returned.

maybe-unfold stop? mapper successor seed ...procedure
either-unfold stop? mapper successor seed ...procedure

If stop? returns true on seeds, a Nothing / a Left of seeds is returned. Otherwise, successor is applied to seeds. If stop? returns false on the results of successor, it is an error. (This is a condition of kind (exn assertion).) But if the second call to stop? returns true, mapper is applied to seeds and the results are wrapped in a Just/Right and returned.

Syntax

(maybe-if maybe-expr just-expr nothing-expr)syntax

Equivalent to (if (just? maybe-expr) just-expr nothing-expr), except that a condition of kind (exn type assertion) is raised if maybe-expr does not evaluate to a Maybe.

(maybe-and expr ...)syntax
(either-and expr ...)syntax

The exprs are evaluated in order. If an expr evaluates to a Nothing / Left, it is returned without evaluating any more expressions. Otherwise, the last Just / Right is returned. If there are no exprs, then a Just/Right of an unspecified value is returned. If an expr does not evaluate to a Maybe/Either, a condition of kind (exn type assertion) is raised.

(maybe-or expr ...)syntax
(either-or expr ...)syntax

The exprs are evaluated in order. If an expr evaluates to a Just/Right, it is returned without evaluating any more expressions. Otherwise, the last Nothing/Left is returned. If there are no exprs, then Nothing / a Left of an unspecified value is returned. If an expr does not evaluate to a Maybe/Either, a condition of kind (exn type assertion) is raised.

(maybe-let* (claw ...) . body)syntax
(either-let* (claw ...) . body)syntax

The Maybe/Either equivalent of and-let* from SRFI 2. However, instead of testing whether a claw is true or false, it tests whether it is Just/Right or Nothing/Left. If a claw does not evaluate to a Maybe/Either, an condition of kind (exn type assertion) is raised. If a claw evaluates to a Just or Right that wraps zero values or more than one value, then a condition of kind (exn type payload) is raised.

A claw is either a bound identifier, or a list of length 1, in which case it is an expression, or a list of length 2, in which case it is a variable and an expression. In either case, the value of the claw is the value of the bound variable or expression. If the value of a claw is Nothing / a Left, the expression immediately returns that value. But if the value of a claw is a Just/Right, then the variable (if any) is bound to its unwrapped value; the scope of this binding is any remaining claws and the body.

If the values of all the claws are Just/Right, then the body is evaluated as if it were in a (let () ...), and its values are wrapped in a Just/Right and returned.

(maybe-let*-values (claw ...) . body)syntax
(either-let*-values (claw ...) . body)syntax

The same as maybe/either-let*, except that the first element of a two-element claw is a <formals> from lambda, and the variables mentioned in it (if any) are bound to the values that result from unpacking a Just/Right. Note that a claw with a single variable in maybe/either-let* is bound to the sole value contained in the Just/Right, whereas in maybe/either-let*-values, it is bound to a list of all the values, just as in a lambda expression, a <formals> consisting of a single variable is bound to a list of all the procedure arguments.

(either-guard pred-expr . body)syntax

The syntactic analogue of exception->either. The body is evaluated, and the values it produces are wrapped in a Right and returned, unless an exception occurs. If the condition object that is raised satisfies the predicate that is the value of pred-expr, the condition object is wrapped in a Left and returned; otherwise the condition is reraised as if by raise-continuable.

Trivalent logic

These procedures provide trivalent logic in the style of SQL on Maybe objects containing a single value. For the purposes of this section, an object counts as false if it is Just #f, true if it is Just anything else. It is an error if any argument is not a Maybe.

Unlike and and or, tri-and and tri-or are procedures and not syntax, because determining their value requires that all the arguments be evaluated in order to provide correct SQL-style semantics. For example, (and #f (nothing)) will return #f immediately without evaluating its second argument, but (tri-and (just #f) (nothing)) will return Nothing.

tri-not maybeprocedure

Returns Just #t if maybe is false, Just #f if maybe is true, and Nothing if maybe is Nothing.

tri=? maybe1 maybe2 ...procedure

Similar to boolean=?, returning Just #t if all the maybes are true or if all are false. Otherwise, if any maybe is Nothing or any two maybes have different (trivalent) truth values, returns Just #f.

Note: this function is not transitive and therefore is not an equivalence relation.

tri-and maybe ...procedure

If all maybes are true, Just #t is returned. If any maybe is false or Nothing, then the first such maybe is returned. If there are no arguments, Just #t is returned.

tri-or maybe ...procedure

If all maybes are false, Just #f is returned. If any maybe is true or Nothing, then the first such maybe is returned. If there are no arguments, Just #f is returned.

tri-merge maybe ...procedure

If any maybes are true or false, then the first such maybe is returned. If all maybes are Nothing, then Nothing is returned. If there are no arguments, Nothing is returned.

Examples

Here is a variant of SRFI 1 find that returns a Maybe, and so can be safely used to search for #f in a list, just like any other value:

(define (maybe-find pred list)
  (cond
    ((null? list) (nothing))
    ((pred (car list)) (just (car list)))
    (else (maybe-find pred (cdr list)))))

The following examples show how find and maybe-find differ in their results:

(define (atom? obj) (not (pair? obj)))
(define with-t '((1) #t (3) 4 5))
(define with-f '((1) #f (3) 4 5))
(define without '((1) (2) (3) (4) (5)))

(find atom? with-t) => #t
(find atom? with-f) => #f
(find atom? without) => #f

(maybe-find atom? with-t) => Just #t
(maybe-find atom? with-f) => Just #f
(maybe-find atom? without) => Nothing

And here is head, which is more general than car because it lets the caller figure out what happens if its argument is not a pair and can handle an argument that is either an actual pair or Just a pair.

(define (head x)
  (cond
    ((pair? x) (just (car x)))
    ((and (just? x) (pair? (maybe-ref x))) (just (car (maybe-ref x))))
    (else (nothing))))

In other words, if the argument to head is a pair, whether wrapped in a Just or not, the result is the car of that pair wrapped in a Just; in all other cases, the result is Nothing. (There are simpler ways to do this using more advanced procedures.)

Exceptions

This egg tries to provide useful information when things go wrong. Type errors raise conditions of kind (exn type assertion), while arity mismatches cause (exn arity assertion) conditions to be raised. Whenever the payload of a Maybe/Either violates a requirement (e.g. that of maybe-join), a condition of kind (exn type payload) is raised.

See Module (chicken condition) for more information.

The SRFI requires that the forms in the Syntax section signal an error that satisfies error-object?; this egg raises more detailed exceptions. This is fine, since CHICKEN's error-object? (in r7rs) is simply an alias for condition?.

About This Egg

Dependencies

The srfi-1 egg is required. The test egg is required to run the included tests.

Maintainer

Wolfgang Corcoran-Mathe <wcm at sigwinch dot xyzzy without the zy>

Repository

GitHub

Version History

0.1 (2020-11)
Initial release.
1.0.0 (2022-08-15)
Rework exceptions, streamline dependencies, improve tests.
1.0.2 (2022-08-16)
Fix the kind of condition raised by maybe->two-values in some cases.
1.0.3 (2022-12-26)
Fix unresolved identifier problem.

License

Copyright (C) John Cowan, Wolfgang Corcoran-Mathe (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.

Contents »