tamer: iter::trip: Introduce initial TripIter concept

See the documentation in this commit for more information.

This is pretty significant, in that it's been a long-standing question for
me how I'd like to join together `Result` iterators without having
unnecessarily complex APIs, and also allow for error recovery.  This solves
both of those problems.

It should be noted, however, that this does not yet explicitly implement
error recovery, beyond being able to observe the failure as the result of
the provided callback function.  Proper recovery will be implemented once
there's a use-case.

DEV-11006
main
Mike Gerwitz 2021-10-28 14:27:33 -04:00
parent 18cadb9c7d
commit f6c5a224c8
3 changed files with 526 additions and 1 deletions

162
tamer/src/iter.rs 100644
View File

@ -0,0 +1,162 @@
// TAMER iterators
//
// Copyright (C) 2014-2021 Ryan Specialty Group, 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
//! [`TokenResultIterator`](crate::ir::xir::TokenResultIterator),
//! 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 `T` 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.
//! Each of those functions provide their own minimal examples,
//! one of which is reproduced here:
//!
//! ```
//! use tamer::iter::with_iter_while_ok;
//!
//! let mut values = [Ok(0), Err("trip"), Ok(1)].into_iter();
//!
//! let result = with_iter_while_ok(&mut values, |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());
//! });
//!
//! // 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`].
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 trip::{into_iter_while_ok, with_iter_while_ok, TripIter};

View File

@ -0,0 +1,356 @@
// Tripping iterator
//
// Copyright (C) 2014-2021 Ryan Specialty Group, 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/>.
//! An iterator that wraps [`ResultIterator`] and trips on the first
//! [`Err`].
//!
//! This acts as a circuit breaker---when
//! an error occurs,
//! the iterator trips to prevent further damage,
//! halting downstream processing while providing the opportunity to
//! inspect and act upon the error.
//!
//! This iterator simplifies the management of [`ResultIterator`]s and
//! allows downstram APIs of those iterators to remain simple without
//! having to concern themselves with error conditions from a source
//! iterator.
//!
//! _Support for immediate error recovery has not yet been added._
//! It will be once it is needed.
//!
//! See the [parent module](super) and [`TripIter`] for more information.
use super::ResultIterator;
use std::marker::PhantomData;
/// An iterator that trips when encountering an [`Err`] on an underling
/// iterator.
///
/// This iterator can be constructed using one of
///
/// 1. [`with_iter_while_ok`] to retain ownership over the source
/// iterator; or
/// 2. [`into_iter_while_ok`] for a more ergonomic API at the cost of
/// ceding ownership over the source iterator.
///
/// See those two functions and the [parent module](super) for more
/// information and examples.
pub struct TripIter<'a, I: ResultIterator<T, E>, T, E> {
/// Iterator that will be unwrapped while its item is [`Ok`].
inner: &'a mut I,
/// The current state of the iterator,
/// including the most recently encountered error.
///
/// If there is an error,
/// then we have been tripped and will return [`None`] until the error
/// condition has been resolved.
state: Result<(), E>,
/// Our [`Iterator`] implementation will yield `T`,
/// but we don't store it.
_phantom: PhantomData<T>,
}
impl<'a, I: ResultIterator<T, E>, T, E> TripIter<'a, I, T, E> {
/// Given a mutable reference to a
/// [`ResultIterator<T, E>`](ResultIterator),
/// yield a [`TripIter`] that yields the inner `T` value while the
/// iterator yields an [`Ok`] item.
///
/// See the public [`with_iter_while_ok`] function for more
/// information.
#[inline]
fn with_iter_while_ok<F, U>(iter: &'a mut I, f: F) -> Result<U, E>
where
F: FnOnce(&mut Self) -> U,
{
let mut biter = Self {
inner: iter,
state: Ok(()),
_phantom: Default::default(),
};
// The TripIter will be available only for the duration of this
// call...
let fret = f(&mut biter);
// ...which ensures that we have the opportunity,
// after the caller is done with it,
// to check to see if we have tripped and force the caller to
// consider the error.
biter.state.and_then(|_| Ok(fret))
}
/// Whether the iterator has been tripped by an [`Err`] on the
/// underlying iterator.
///
/// Until a recovery mechanism is provided,
/// the [`Err`] is available through one of [`with_iter_while_ok`] or
/// [`into_iter_while_ok`].
#[inline]
pub fn is_tripped(&self) -> bool {
self.state.is_err()
}
}
impl<'a, I, T, E> Iterator for TripIter<'a, I, T, E>
where
I: Iterator<Item = Result<T, E>>,
{
type Item = T;
/// Unwrap and return the next [`Ok`] result from the underlying
/// iterator,
/// otherwise [`None`].
///
/// Once an [`Err`] is encountered,
/// this iterator trips and will continue to return [`None`] until the
/// error state is resolved.
/// See [`with_iter_while_ok`] for more information.
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.is_tripped() {
return None;
}
match self.inner.next() {
Some(Ok(value)) => Some(value),
Some(Err(err)) => {
self.state = Err(err);
None
}
None => None,
}
}
}
/// Given a mutable reference to a [`ResultIterator<T, E>`](ResultIterator),
/// yield a [`TripIter`] that yields the inner `T` value while the
/// iterator yields an [`Ok`] item.
///
/// Once an [`Err`] is encountered,
/// [`TripIter`] trips,
/// yielding [`None`] until the error condition is resolved.
/// This allows for a recovery mechanism that resumes computation,
/// provided that the system expects [`TripIter`] to be resumable.
///
/// The [`TripIter`] is provided via a callback `f`,
/// and is valid only for its duration.
/// This allows us to return either the most recently encountered [`Err`],
/// otherwise [`Ok`] with the return value of `f`,
/// ensuring that the error causing the trip will not be lost.
///
/// This function accepts a mutable reference to the underlying iterator,
/// allowing the caller to retain ownership for further processing after
/// iteration completes.
/// If this is not needed
/// (e.g. all processing will be performed in `f`),
/// [`into_iter_while_ok`] may provide a more ergonomic API at the cost of
/// ownership.
///
/// ```
/// use tamer::iter::with_iter_while_ok;
///
/// let mut values = [Ok(0), Err("trip"), Ok(1)].into_iter();
///
/// let result = with_iter_while_ok(&mut values, |iter| {
/// // First is `Ok`, so it yields.
/// 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());
/// });
///
/// // 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());
/// ```
///
/// See the [parent module](super) for more information and examples.
#[inline]
pub fn with_iter_while_ok<'a, I, T, U, E, F>(
from: &'a mut I,
f: F,
) -> Result<U, E>
where
I: ResultIterator<T, E>,
F: FnOnce(&mut TripIter<'a, I, T, E>) -> U,
{
TripIter::with_iter_while_ok(from, f)
}
/// Given an object capable of being converted into a
/// [`ResultIterator<T, E>`](ResultIterator),
/// yield a [`TripIter`] that yields the inner `T` value while the
/// iterator yields an [`Ok`] item.
///
/// This is a more ergonomic form of [`with_iter_while_ok`] if ownership
/// over the `from` iterator can be ceded;
/// see that function for more information.
///
/// ```
/// use tamer::iter::into_iter_while_ok;
///
/// let values = [Ok(0), Err("trip"), Ok(1)];
///
/// let result = into_iter_while_ok(values, |iter| {
/// // First is `Ok`, so it yields.
/// 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());
/// });
///
/// // The error that caused the trip is returned.
/// assert_eq!(Err("trip"), result);
///
/// // But we cannot access the remainder of the iterator, having
/// // lost ownership. See `with_iter_while_ok` if this is needed.
/// ```
///
/// See the [parent module](super) for more information and examples.
#[inline]
pub fn into_iter_while_ok<I, T, U, E, F>(from: I, f: F) -> Result<U, E>
where
I: IntoIterator<Item = Result<T, E>>,
F: FnOnce(&mut TripIter<I::IntoIter, T, E>) -> U,
{
with_iter_while_ok(&mut from.into_iter(), f)
}
#[cfg(test)]
mod test {
use super::*;
type TestResult<E = ()> = Result<(), E>;
#[test]
fn inner_none_yields_none() -> TestResult {
let empty = Vec::<Result<(), ()>>::new();
into_iter_while_ok(empty, |sut| {
assert_eq!(None, sut.next());
})
}
#[test]
fn ok_yields_unwrapped_values() -> TestResult {
let value1 = "inner1";
let value2 = "inner2";
into_iter_while_ok([Ok(value1), Ok(value2)], |sut| {
assert_eq!(Some(value1), sut.next());
assert_eq!(Some(value2), sut.next());
assert_eq!(None, sut.next());
assert!(!sut.is_tripped());
})
}
#[test]
fn err_yields_none_and_trips() {
let value = "okay value";
let err = "tripped";
let result =
into_iter_while_ok([Ok(value), Err(err), Ok(value)], |sut| {
// The first value is `Ok`,
// but the second is `Err` which trips the breaker and does not
// yield any further elements.
assert_eq!(Some(value), sut.next());
assert!(!sut.is_tripped());
// Trips here.
assert_eq!(None, sut.next());
assert!(sut.is_tripped());
// Nor should it ever, while tripped.
assert_eq!(None, sut.next());
assert!(sut.is_tripped());
});
assert_eq!(result, Err(err));
}
#[test]
fn trip_stops_consuming_iter() {
let mut values = [Err("trip"), Ok(1)].into_iter();
assert_eq!(
Err("trip"),
with_iter_while_ok(&mut values, |sut| {
// Try to consume everything.
assert_eq!(None, sut.next());
assert_eq!(None, sut.next());
}),
);
// Should have only consumed up to the `Err`.
assert_eq!(Some(Ok(1)), values.next());
}
#[test]
fn open_returns_f_ret_after_none() {
let ret = "retval";
assert_eq!(
Ok(ret),
into_iter_while_ok([Result::<_, ()>::Ok(())], |sut| {
sut.next();
ret
})
);
}
#[test]
fn tripped_ignores_f_ret_after_none() {
let err = "tripped ignore ret";
assert_eq!(
Err(err),
into_iter_while_ok([Result::<(), _>::Err(err)], |sut| {
sut.next();
"not used"
})
);
}
// If we don't call `next`,
// then we haven't hit the error and so it should not trip.
#[test]
fn does_not_trip_before_err_is_encountered() {
let ret = "no next";
assert_eq!(
Ok(ret),
into_iter_while_ok(
[Result::<(), _>::Err("will not be seen")],
|_| { "no next" }
)
);
}
}

View File

@ -27,7 +27,13 @@
// See that function for more information.
#![feature(const_fn_trait_bound)]
#![feature(const_transmute_copy)]
// We build docs for private items
// Trait aliases are convenient for reducing verbosity in situations where
// type aliases cannot be used.
// To remove this feature if it is not stabalized,
// simply replace each alias reference with its definition,
// or possibly write a trait with a `Self` bound.
#![feature(trait_alias)]
// We build docs for private items.
#![allow(rustdoc::private_intra_doc_links)]
pub mod global;
@ -45,6 +51,7 @@ pub mod convert;
pub mod fs;
#[macro_use]
pub mod ir;
pub mod iter;
pub mod ld;
pub mod obj;
pub mod span;