From d3213f076e8cdbb33f60d335a93b5e3f377021d6 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Tue, 29 Oct 2024 00:38:15 +0100 Subject: [PATCH] findFirstIndex.nix: init --- flake.nix | 1 + src/findFirstIndex.nix | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/findFirstIndex.nix diff --git a/flake.nix b/flake.nix index 7f74a34..4e51d15 100644 --- a/flake.nix +++ b/flake.nix @@ -15,5 +15,6 @@ subtractLists = import ./src/subtractLists.nix lib; unique = import ./src/unique.nix lib; overrideExisting = import ./src/overrideExisting.nix lib; + findFirstIndex = import ./src/findFirstIndex.nix lib; }; } diff --git a/src/findFirstIndex.nix b/src/findFirstIndex.nix new file mode 100644 index 0000000..3286eca --- /dev/null +++ b/src/findFirstIndex.nix @@ -0,0 +1,72 @@ +lib: +let + findFirstIndexOld = + pred: + default: + list: + 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 = lib.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 + resultIndex; + + findFirstIndexNew = + pred: + default: + list: + if list == [ ] then default else + let + stopAtListLength = lib.min (builtins.length list - 1); + closure = builtins.genericClosure (rec { + startSet = [{ + key = -1; + found = false; + }]; + operator = item: if item.found then startSet else [(rec { + key = stopAtListLength (item.key + 1); + found = pred (builtins.elemAt list key); + })]; + }); + lastItem = builtins.elemAt closure (builtins.length closure - 1); + in if lastItem.found then lastItem.key else default; + + bigdata = (lib.imap0 (i: _: toString i) (lib.replicate 999999 null)); + + tests = f: { + none = f (x: x == ":)") "not found" bigdata; + last = f (x: x == ":)") "not found" (bigdata ++ [":)"]); + middle = f (x: x == ":)") "not found" (bigdata ++ [":)"] ++ bigdata); + empty = f (x: x == ":)") "not found" [ ]; + }; +in rec { + old = tests findFirstIndexOld; + new = tests findFirstIndexNew; + sanityCheck = lib.all (name: assert lib.assertMsg (old.${name} == new.${name}) "output of '${name}' test is not equal"; true) (builtins.attrNames (tests null)); +}