tame/tamer/src/iter.rs

288 lines
10 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

// TAMER iterators
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
// This file is part of TAME.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Iterators for common problems.
//!
//! TAMER makes heavy use of iterators for data streams and lowering
//! operations.
//!
//! [`Result`] Iterators
//! ====================
//! Iterators that can fail,
//! such as XIR's
//! [`TokenResultStream`](crate::xir::TokenResultStream),
//! can be confounding and difficult to work with because
//! [`Iterator::next`] wraps the [`Result`] within an [`Option`].
//! Further,
//! if we were to directly pipe the results of one iterator to another,
//! then downstream iterators would have to worry about handling failures
//! of their source iterators just for the sake of propagation,
//! forcing them to accept a [`Result`] iterator rather than an
//! iterator producing the inner type that they are interested in.
//!
//! [`ResultIterator`] exists to slightly reduce cognitive load in code with
//! complex types;
//! it's simply an alias for `Iterator<Item = Result<T, E>>`.
//!
//! Managing Failures
//! -----------------
//! The [`TripIter`] iterator helps to simplify APIs and provides the
//! opportunity for error correction by yielding the inner value of [`Ok`]
//! on an underlying iterator,
//! tripping if it encounters an [`Err`].
//! This is analogous to a circuit breaker,
//! protecting downstream subsystems from faulty data.
//!
//! When a trip happens,
//! the [`TripIter`] yields [`None`].
//! Downstream systems must determine for themselves whether receiving
//! [`None`] should constitute an error,
//! or whether they should wait around until the iterator can be resumed
//! to continue processing.
//!
//! Put simply: we take an `Iterator<Item = Result<T, E>` and produce an
//! `Iterator<Item = T>`,
//! so that consumers of `T` needn't know or care that we could fail to
//! produce a `T`.
//!
//! This iterator is constructed using either [`with_iter_while_ok`] or
//! [`into_iter_while_ok`],
//! depending on whether ownership of the source iterator needs to be
//! retained.
//! The [`TrippableIterator`] trait also provides convenient methods
//! directly on compatible [`Iterator`]s.
//! Each of those functions provide their own minimal examples,
//! one of which is reproduced here:
//!
//! ```
//! use tamer::iter::TrippableIterator;
//!
//! let mut values = [Ok(0), Err("trip"), Ok(1)].into_iter();
//!
//! let result = values.while_ok(|iter| {
//! // First is `Ok`, so it yields. Note that the value is no longer
//! // `Ok`, which liberates our system from handling others' errors.
//! assert_eq!(Some(0), iter.next());
//! assert!(!iter.is_tripped());
//!
//! // But the next is an `Err`, so it trips and returns `None`
//! // from that point onward.
//! assert_eq!(None, iter.next());
//! assert_eq!(None, iter.next());
//! assert!(iter.is_tripped());
//!
//! Ok(())
//! });
//!
//! // The error that caused the trip is returned.
//! assert_eq!(Err("trip"), result);
//!
//! // We still have access to the iterator where it left off.
//! assert_eq!(Some(Ok(1)), values.next());
//! ```
//!
//! Each of these functions take a callback;
//! the [`TripIter`] is valid only for the duration of that function.
//! This allows us to know when the caller is finished with the iterator so
//! that we can determine whether it tripped and,
//! if so,
//! yield the associated [`Result`] to force the caller to consider what
//! happened.
//! This allows for out-of-band error processing---we
//! still get to handle and propagate the error,
//! but alleviate the system using [`TripIter`] from having to know that
//! source failures were even possible.
//!
//! Alternative Failure Modes
//! -------------------------
//! [`TripIter`] is only needed for high-performance lazy processing of data
//! streams where error recovery may be needed.
//! If that's not your case,
//! Rust has built-in features that may suit your needs.
//!
//! For example,
//! if you wish to collect iterator results but yield an [`Err`] if any of
//! those fail,
//! you can use [`Iterator::collect`]:
//!
//! ```
//! // Contains an `Err`, and so fails.
//! let values = [Ok(1), Err("bad")].into_iter();
//! assert_eq!(Err("bad"), values.collect::<Result<Vec<_>, _>>());
//!
//! // All values are `Ok`.
//! let values = [Ok(1), Ok(2)].into_iter();
//! assert_eq!(Ok(vec![1, 2]), values.collect::<Result<Vec<_>, ()>>());
//! ```
//!
//! But this allocates data on the heap,
//! which is unacceptable for stream processing.
//!
//! Another option is to halt at the fist sign of trouble:
//!
//! ```
//! let values = [Ok(1), Ok(2), Err("bad")].into_iter();
//!
//! // Grab everything up to the first error.
//! assert_eq!(vec![1, 2], values.map_while(Result::ok).collect::<Vec<_>>());
//! ```
//!
//! Similar can be done with [`Iterator::take_while`].
//! But this places the responsibility of doing the right thing on the caller,
//! and can generate unwieldy types that are difficult to store in structs
//! (e.g. closure types that cannot be written out)
//! without resorting to boxing,
//! even with Rust's `impl Trait` feature.
//!
//! There are other options,
//! but they also result in boilerplate that is handled for you by
//! [`TripIter`].
//!
//!
//! Failliable Collecting
//! =====================
//! [`Iterator::collect`] provides a powerful interface that can remove a
//! lot of boilerplate or manual composition,
//! but is not designed to handle failures.
//! This becomes problematic when attempting to encapsulate failure logic
//! within [`FromIterator`] implementations because of foreign traits.
//! [`TryFromIterator`] provides a [`Result`] return type for the collection
//! itself.
//!
//! This is useful when an object aggregates data from an iterator in an
//! all-or-nothing manner,
//! or wishes to reject data it does not support.
//!
//! Consider this example where we have a set attributes from which we wish
//! to generate a `Variable` object consisting of a name and value.
//! This object expects both of these attributes to be present,
//! and further expects that no other attributes will be provided.
//! This effectively defines a basic schema.
//!
//! ```
//! use tamer::iter::{TryCollect, TryFromIterator};
//! use tamer::sym::{GlobalSymbolIntern, SymbolId};
//! use tamer::sym::st::raw::{L_NAME, L_VALUE};
//!
//! struct Attr(SymbolId, SymbolId);
//!
//! #[derive(Debug, PartialEq, Eq)]
//! struct Variable {
//! name: SymbolId,
//! value: SymbolId,
//! }
//!
//! #[derive(Debug, PartialEq, Eq)]
//! enum VariableError {
//! UnknownAttr(SymbolId),
//! MissingName,
//! MissingValue,
//! }
//!
//! impl TryFromIterator<Attr> for Variable {
//! type Error = VariableError;
//!
//! fn try_from_iter<I: IntoIterator<Item = Attr>>(
//! iter: I,
//! ) -> Result<Self, Self::Error> {
//! let mut name = None;
//! let mut value = None;
//!
//! for Attr(attr_name, attr_value) in iter.into_iter() {
//! match attr_name {
//! L_NAME => { name.replace(attr_value); },
//! L_VALUE => { value.replace(attr_value); },
//! _ => return Err(VariableError::UnknownAttr(attr_name)),
//! }
//! }
//!
//! Ok(Variable {
//! name: name.ok_or(VariableError::MissingName)?,
//! value: value.ok_or(VariableError::MissingValue)?,
//! })
//! }
//! }
//!
//! let name = "foo".intern();
//! let value = "bar".intern();
//!
//! assert_eq!(
//! Ok(Variable { name, value }),
//! vec![Attr(L_NAME, name), Attr(L_VALUE, value)].into_iter().try_collect(),
//! );
//!
//! assert_eq!(
//! Err(VariableError::MissingName),
//! vec![Attr(L_VALUE, value)].into_iter().try_collect::<Variable>(),
//! );
//! ```
//!
//! This cannot be done with [`FromIterator`] because it would require
//! `impl FromIterator<Attr> for Result<Variable, VariableError>`,
//! both of which are foreign traits.
//! Perhaps this restriction will be relaxed in Rust in the future,
//! rendering [`TryFromIterator`] obsolete.
//!
//! [`TryFromIterator`] is implemented for all [`FromIterator`],
//! allowing [`try_collect`] to accept all types that [`collect`] accepts.
//! Type inference maintains the powerful sentient illusion that [`collect`]
//! provides:
//!
//! ```
//! use tamer::iter::TryCollect;
//!
//! // Wraps `Iterator::collect`.
//! assert_eq!(
//! Ok(Some(vec![1, 2, 3])),
//! vec![Some(1), Some(2), Some(3)].into_iter().try_collect()
//! );
//!
//! // Does _not_ unwrap `Result` from `Iterator::collect`.
//! assert_eq!(
//! Ok(Err("fail")),
//! vec![Ok(1), Err("fail")].into_iter().try_collect::<Result<Vec<_>, _>>()
//! );
//! ```
//!
//! Notably, [`try_collect`] _does not_ unwrap [`Result`] from [`collect`].
//! This makes sense from a type perspective,
//! but may seem unintuitive at a glance.
//!
//! If the source iterator yields [`Result<T, E>`](Result),
//! but the [`TryFromIterator`] expects `T`,
//! see [`TryCollect::try_collect_ok`].
//!
//! [`try_collect`]: TryCollect::try_collect
//! [`collect`]: Iterator::collect
mod collect;
mod trip;
/// An [`Iterator`] over [`Result`]s.
///
/// Since [`Iterator::next`] returns [`Option`],
/// this results in a return value of [`Option<Result<T, E>>`],
/// which is confusing and inconvenient to work with.
pub trait ResultIterator<T, E> = Iterator<Item = Result<T, E>>;
pub use collect::{TryCollect, TryFromIterator};
pub use trip::{
into_iter_while_ok, with_iter_while_ok, TripIter, TrippableIterator,
};