Unverified Commit 6b078d2f authored by Robert Hensing's avatar Robert Hensing Committed by GitHub
Browse files

Merge pull request #235267 from tweag/lazier-findFirst

`lib.findFirst`: Add tests and make lazier
parents b52f6ac6 9790e701
Loading
Loading
Loading
Loading
+32 −2
Original line number Diff line number Diff line
@@ -198,8 +198,38 @@ rec {
    default:
    # Input list
    list:
    let found = filter pred list;
    in if found == [] then default else head found;
    let
      # A naive recursive implementation would be much simpler, but
      # would also overflow the evaluator stack. We use `foldl'` as a workaround
      # because it reuses the same stack space, evaluating the function for one
      # element after another. We can't return early, so this means that we
      # sacrifice early cutoff, but that appears to be an acceptable cost. A
      # clever scheme with "exponential search" is possible, but appears over-
      # engineered for now. See https://github.com/NixOS/nixpkgs/pull/235267

      # Invariant:
      # - if index < 0 then el == elemAt list (- index - 1) and all elements before el didn't satisfy pred
      # - if index >= 0 then pred (elemAt list index) and all elements before (elemAt list index) didn't satisfy pred
      #
      # We start with index -1 and the 0'th element of the list, which satisfies the invariant
      resultIndex = foldl' (index: el:
        if index < 0 then
          # No match yet before the current index, we need to check the element
          if pred el then
            # We have a match! Turn it into the actual index to prevent future iterations from modifying it
            - index - 1
          else
            # Still no match, update the index to the next element (we're counting down, so minus one)
            index - 1
        else
          # There's already a match, propagate the index without evaluating anything
          index
      ) (-1) list;
    in
    if resultIndex < 0 then
      default
    else
      elemAt list resultIndex;

  /* Return true if function `pred` returns true for at least one
     element of `list`.
+40 −0
Original line number Diff line number Diff line
@@ -518,6 +518,46 @@ runTests {
    expected = false;
  };

  testFindFirstExample1 = {
    expr = findFirst (x: x > 3) 7 [ 1 6 4 ];
    expected = 6;
  };

  testFindFirstExample2 = {
    expr = findFirst (x: x > 9) 7 [ 1 6 4 ];
    expected = 7;
  };

  testFindFirstEmpty = {
    expr = findFirst (abort "when the list is empty, the predicate is not needed") null [];
    expected = null;
  };

  testFindFirstSingleMatch = {
    expr = findFirst (x: x == 5) null [ 5 ];
    expected = 5;
  };

  testFindFirstSingleDefault = {
    expr = findFirst (x: false) null [ (abort "if the predicate doesn't access the value, it must not be evaluated") ];
    expected = null;
  };

  testFindFirstNone = {
    expr = builtins.tryEval (findFirst (x: x == 2) null [ 1 (throw "the last element must be evaluated when there's no match") ]);
    expected = { success = false; value = false; };
  };

  # Makes sure that the implementation doesn't cause a stack overflow
  testFindFirstBig = {
    expr = findFirst (x: x == 1000000) null (range 0 1000000);
    expected = 1000000;
  };

  testFindFirstLazy = {
    expr = findFirst (x: x == 1) 7 [ 1 (abort "list elements after the match must not be evaluated") ];
    expected = 1;
  };

# ATTRSETS