Skip to content

Commit

Permalink
Merge pull request #226 from 01mf02/flat-map-then-strict
Browse files Browse the repository at this point in the history
Make `flat_map_then` strict.
  • Loading branch information
01mf02 authored Nov 11, 2024
2 parents e972648 + 305cb55 commit e0aa1a3
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 23 deletions.
65 changes: 43 additions & 22 deletions jaq-core/src/box_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@ pub fn then<'a, T, U: 'a, E: 'a>(
x.map_or_else(|e| box_once(Err(e)), f)
}

/// Return next element if iterator returns at most one element, else `None`.
///
/// This is one of the most important functions for performance in jaq.
/// It enables optimisations for the case when a filter yields exactly one output,
/// which is very common in typical jq programs.
///
/// For example, for the filter `f | g`, this function is called on the outputs of `f`.
/// If `f` yields a single output `y`, then the output of `f | g` is `y` applied to `g`.
/// This has two beneficial consequences:
///
/// 1. We can estimate the number of outputs of `f | g` by estimating the outputs of `g`.
/// (In general, we cannot do this even when we know the number of outputs of `f`,
/// because `g` might yield a different number of elements for each output of `f`.)
/// 2. We do not need to clone the context when passing it to `g`,
/// because we know that `g` is only called once.
///
/// This optimisation applies to many other filters as well.
///
/// To see the impact of this function, you can replace its implementation with just `None`.
/// This preserves correctness, but can result in severely degraded performance.
fn next_if_one<T>(iter: &mut impl Iterator<Item = T>) -> Option<T> {
if iter.size_hint().1 == Some(1) {
let ly = iter.next()?;
// the Rust documentation states that
// "a buggy iterator may yield [..] more than the upper bound of elements",
// but so far, it seems that all iterators here are not buggy :)
debug_assert!(iter.next().is_none());
return Some(ly);
}
None
}

/// For every element `y` returned by `l`, return the output of `r(y, x)`.
///
/// In case that `l` returns only a single element, this does not clone `x`.
Expand All @@ -29,17 +61,10 @@ pub fn map_with<'a, T: Clone + 'a, U: 'a, V: 'a>(
x: T,
r: impl Fn(U, T) -> V + 'a,
) -> BoxIter<'a, V> {
// this special case is to avoid cloning `x`
if l.size_hint().1 == Some(1) {
if let Some(ly) = l.next() {
// the Rust documentation states that
// "a buggy iterator may yield [..] more than the upper bound of elements",
// but so far, it seems that all iterators here are not buggy :)
assert!(l.next().is_none());
return box_once(r(ly, x));
}
match next_if_one(&mut l) {
Some(ly) => box_once(r(ly, x)),
None => Box::new(l.map(move |ly| r(ly, x.clone()))),
}
Box::new(l.map(move |ly| r(ly, x.clone())))
}

/// For every element `y` returned by `l`, return the outputs of `r(y, x)`.
Expand All @@ -50,25 +75,21 @@ pub fn flat_map_with<'a, T: Clone + 'a, U: 'a, V: 'a>(
x: T,
r: impl Fn(U, T) -> BoxIter<'a, V> + 'a,
) -> BoxIter<'a, V> {
// this special case is to avoid cloning `x`
if l.size_hint().1 == Some(1) {
if let Some(ly) = l.next() {
// the Rust documentation states that
// "a buggy iterator may yield [..] more than the upper bound of elements",
// but so far, it seems that all iterators here are not buggy :)
assert!(l.next().is_none());
return Box::new(r(ly, x));
}
match next_if_one(&mut l) {
Some(ly) => Box::new(r(ly, x)),
None => Box::new(l.flat_map(move |ly| r(ly, x.clone()))),
}
Box::new(l.flat_map(move |ly| r(ly, x.clone())))
}

/// Combination of [`Iterator::flat_map`] and [`then`].
pub fn flat_map_then<'a, T: 'a, U: 'a, E: 'a>(
l: impl Iterator<Item = Result<T, E>> + 'a,
mut l: impl Iterator<Item = Result<T, E>> + 'a,
r: impl Fn(T) -> Results<'a, U, E> + 'a,
) -> Results<'a, U, E> {
Box::new(l.flat_map(move |y| then(y, |y| r(y))))
match next_if_one(&mut l) {
Some(ly) => then(ly, r),
None => Box::new(l.flat_map(move |y| then(y, |y| r(y)))),
}
}

/// Combination of [`flat_map_with`] and [`then`].
Expand Down
2 changes: 1 addition & 1 deletion jaq-core/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ const ACKERMANN: &str = "def ack($m; $n):
else ack($m-1; ack($m; $n-1))
end;";

yields!(ackermann, &(ACKERMANN.to_owned() + "ack(3; 4)"), 125);
yields!(ackermann, &(ACKERMANN.to_owned() + "ack(3; 3)"), 61);

#[test]
fn reduce() {
Expand Down
2 changes: 2 additions & 0 deletions jaq-std/tests/funs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ fn limit() {
give(json!(null), "[limit(-1; 0, 1)]", json!([]));
}

yields!(limit_overflow, "[limit(0; def f: f | .; f)]", json!([]));

yields!(
math_0_argument_scalar_filters,
"[-2.2, -1.1, 0, 1.1, 2.2 | sin as $s | cos as $c | $s * $s + $c * $c]",
Expand Down

0 comments on commit e0aa1a3

Please sign in to comment.