Repository: remeda/remeda
Pull request #672 (2024-05-05 15:20:28)

feat(v2): reduceLazy -> purryFromLazy, +takeLast, lazy zip, built-in flat, and more...
Following re-reading the codebase before releasing v2, I went over a few more things I wanted to change:

* `take` and `drop` shouldn't use reduceLazy.
* `drop` can optimize the trivial lazy case
* `flat` uses the built-in flat.
* `zipWith` was funky, now less funky, and lazy.
* lazy `zip`
* optimize lazy `first`.
* Add takeLast
* Don't use copy-and-splice where slice makes sense (`takeLast`, `dropLast`, `splitAt`).
* don't use internal versions where a simple JS check is enough (`isEmpty`)
* Update migration docs for `mapToObj`
* bump ts minimum version.
* Remove `reduceLazy`. It was reimplementing parts of `pipe`'s logic; instead, now, we rely on the way pipe handles lazy and build the rest of the APIs (dataFirst, dataLast) from it.

codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/remeda/remeda/pr/672/builds/501446) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 9d583da99308b1cf39c2d2918a2d02ab8b41f783:

|Sandbox| Source |
|--|--|
|[remeda-example-vanilla](https://codesandbox.io/s/remeda-example-vanilla-h2dk4f)| Configuration |


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 02e96f2..94bc743 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,16 +18,11 @@ jobs:
           - "~5.4.0"
           - "~5.3.0"
           - "~5.2.0"
-          # We use a `"module": "NodeNext"` in our tsconfig when we test our
-          # distribution types, which was introduced in TypeScript v4.7, so that
-          # is the earliest version we can test our code in. This defines the
-          # minimal TypeScript version we support.
-          # IMPORTANT: Increasing the minimal version might require us to bump
-          # a MAJOR version of Remeda! Any changes here should be discussed with
-          # the rest of the maintainers.
-          - "~4.9.0"
-          - "~4.8.0"
-          - "~4.7.0"
+          # This is the current lowest version of typescript we support. Do not
+          # change this without bumping a major version. We support up to 4
+          # versions back from the latest version of typescript at time of a
+          # major release.
+          - "~5.1.0"
 
     steps:
       - name: ⬇️ Checkout repo
diff --git a/docs/src/components/function-card.astro b/docs/src/components/function-card.astro
index 07923b9..f76d202 100644
--- a/docs/src/components/function-card.astro
+++ b/docs/src/components/function-card.astro
@@ -3,9 +3,11 @@ import FunctionApi from "@/components/function-api.astro";
 import { Card, CardContent, CardHeader } from "@/components/ui/card";
 import type { DocumentedFunction } from "@/lib/transform";
 import FunctionDescription from "./function-description.astro";
+import type { ReactNode } from "react";
 
 type Props = {
   readonly func: DocumentedFunction;
+  readonly children?: ReactNode;
 };
 
 const { func } = Astro.props;
diff --git a/docs/src/components/header.astro b/docs/src/components/header.astro
index 84568ab..5d87478 100644
--- a/docs/src/components/header.astro
+++ b/docs/src/components/header.astro
@@ -37,7 +37,7 @@ const { pathname } = Astro.url;
         class:list={[
           "transition-colors hover:text-foreground/80",
           pathname === "/v1/" ? "text-foreground" : "text-foreground/60",
-        ]}>Migrate from v1</a
+        ]}>Docs (v1) + Migration Guide</a
       >
     </nav>
     <div class="-mr-2 ml-auto flex items-center gap-2">
diff --git a/docs/src/components/markdown.astro b/docs/src/components/markdown.astro
index bbac718..ebc734c 100644
--- a/docs/src/components/markdown.astro
+++ b/docs/src/components/markdown.astro
@@ -3,13 +3,14 @@ import { asHtml } from "@/lib/as-html";
 
 type Props = {
   readonly markdown?: string;
+  readonly className?: string;
 };
 
-const { markdown } = Astro.props;
+const { markdown, className } = Astro.props;
 ---
 
 <article
-  class="prose prose-slate dark:prose-invert prose-code:text-primary prose-code:before:content-none prose-code:after:content-none"
+  class={`prose prose-slate dark:prose-invert prose-code:text-primary prose-code:before:content-none prose-code:after:content-none ${className ?? ""}`}
   set:html={markdown === undefined ? undefined : asHtml(markdown)}
 >
   <slot />
diff --git a/docs/src/components/v1/migration-box.astro b/docs/src/components/v1/migration-box.astro
deleted file mode 100644
index df6b338..0000000
--- a/docs/src/components/v1/migration-box.astro
+++ /dev/null
@@ -1,16 +0,0 @@
----
-import Markdown from "../markdown.astro";
----
-
-<div
-  class="flex flex-col gap-8 rounded-md border border-sky-100 bg-gradient-to-tl from-sky-50 to-sky-100 p-3 shadow-md dark:border-sky-900 dark:from-sky-900 dark:to-sky-950"
->
-  <h6
-    class="text-xs font-semibold uppercase tracking-wide text-neutral-800 underline dark:text-neutral-200"
-  >
-    Breaking Changes in v2
-  </h6>
-  <Markdown>
-    <slot />
-  </Markdown>
-</div>
diff --git a/docs/src/components/v1/migration-box.tsx b/docs/src/components/v1/migration-box.tsx
new file mode 100644
index 0000000..8de3de8
--- /dev/null
+++ b/docs/src/components/v1/migration-box.tsx
@@ -0,0 +1,35 @@
+import type { ReactNode } from "react";
+import { Button } from "@/components/ui/button";
+import {
+  CollapsibleContent,
+  Collapsible,
+  CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { CaretSortIcon } from "@radix-ui/react-icons";
+
+export const MigrationBox = ({
+  children,
+}: {
+  readonly children: ReactNode;
+}): ReactNode => {
+  return (
+    <div className="flex flex-col gap-8 rounded-md border border-sky-100 bg-gradient-to-tl from-sky-50 to-sky-100 p-3 shadow-sm dark:border-sky-900 dark:from-sky-900 dark:to-sky-950">
+      <Collapsible>
+        <div className="relative flex items-center">
+          <h6 className="text-sm font-semibold uppercase tracking-wider text-neutral-800 dark:text-neutral-200">
+            Breaking changes in v2
+          </h6>
+
+          <CollapsibleTrigger asChild className="absolute right-0">
+            <Button size="sm">
+              <span className="sr-only">Expand</span>
+              <CaretSortIcon className="h-4 w-4" />
+            </Button>
+          </CollapsibleTrigger>
+        </div>
+
+        <CollapsibleContent className="pt-4">{children}</CollapsibleContent>
+      </Collapsible>
+    </div>
+  );
+};
diff --git a/docs/src/components/v1/v1-function-card.astro b/docs/src/components/v1/v1-function-card.astro
index 44476c4..5250bbc 100644
--- a/docs/src/components/v1/v1-function-card.astro
+++ b/docs/src/components/v1/v1-function-card.astro
@@ -2,7 +2,8 @@
 import FunctionCard from "@/components/function-card.astro";
 import type { DocumentedFunction } from "@/lib/transform";
 import { getEntry } from "astro:content";
-import MigrationBox from "./migration-box.astro";
+import { MigrationBox } from "./migration-box";
+import Markdown from "../markdown.astro";
 
 const MIGRATION_COLLECTION = "v1-migration";
 
@@ -16,8 +17,18 @@ const entry = await getEntry(MIGRATION_COLLECTION, func.name.toLowerCase());
 const Migration = await entry?.render();
 ---
 
-<FunctionCard func={func}>
-  <MigrationBox>
-    {Migration !== undefined ? <Migration.Content /> : <em>(none)</em>}
-  </MigrationBox>
-</FunctionCard>
+<>
+  {
+    Migration === undefined ? (
+      <FunctionCard func={func} />
+    ) : (
+      <FunctionCard func={func}>
+        <MigrationBox client:load>
+          <Markdown className="max-w-none prose-p:max-w-prose prose-h1:text-xl prose-h1:font-bold prose-h2:text-lg prose-h2:font-semibold prose-h2:mb-2 prose-h3:text-base prose-h3:font-semibold">
+            <Migration.Content />
+          </Markdown>
+        </MigrationBox>
+      </FunctionCard>
+    )
+  }
+</>
diff --git a/docs/src/content/v1-migration/_mapToObj.md b/docs/src/content/v1-migration/_mapToObj.md
deleted file mode 100644
index 579eaa5..0000000
--- a/docs/src/content/v1-migration/_mapToObj.md
+++ /dev/null
@@ -1,1 +0,0 @@
-# TODO: We might remove this function
diff --git a/docs/src/content/v1-migration/mapToObj.md b/docs/src/content/v1-migration/mapToObj.md
new file mode 100644
index 0000000..8845ef3
--- /dev/null
+++ b/docs/src/content/v1-migration/mapToObj.md
@@ -0,0 +1,44 @@
+# Typing
+
+The `indexed` variant was removed; the base implementation takes the same
+parameters. If you are using `indexed` you can simply remove it without any
+other changes.
+
+# Runtime
+
+The mapper now takes 2 additional parameters: `index` - The index of the current
+element being processed in array, and `data` - the array the function was called
+upon (the same signature the callbacks the built-in `Array.prototype` functions
+have).
+
+If you are using a function reference for the mapper (and not an inline arrow
+function), and that function accepts more than one param you might run into
+compile-time (or run-time!) issues because of the extra params being sent on
+each invocation of the function. We highly recommend using [unicorn/no-array-callback-reference](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-callback-reference.md)
+to warn against these issues.
+
+## Examples
+
+### Indexed variant removed
+
+```ts
+// Was
+mapToObj.indexed(array, mapper);
+
+// Now
+mapToObj(array, mapper);
+```
+
+### Potential bug
+
+```ts
+function callback(key: string, index = 0) {
+  return [key, index];
+}
+
+// Bug
+mapToObj(["a", "b", "c"], callback); // => { a: 0, b: 1, c: 2 }, Was: { a: 0, b: 0, c: 0 }
+
+// Fix
+mapToObj(["a", "b", "c"], (item) => callback(item)); // => { a: 0, b: 0, c: 0 }
+```
diff --git a/docs/src/content/v1-migration/mapWithFeedback.md b/docs/src/content/v1-migration/mapWithFeedback.md
index 64436ad..eb2500d 100644
--- a/docs/src/content/v1-migration/mapWithFeedback.md
+++ b/docs/src/content/v1-migration/mapWithFeedback.md
@@ -11,7 +11,7 @@ element being processed in array, and `data` - the array the function was called
 upon (the same signature the callbacks the built-in `Array.prototype` functions
 have).
 
-If you are using a function reference for the predicate (and not an inline arrow
+If you are using a function reference for the mapper (and not an inline arrow
 function), and that function accepts more than one param you might run into
 compile-time (or run-time!) issues because of the extra params being sent on
 each invocation of the function. We highly recommend using [unicorn/no-array-callback-reference](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-callback-reference.md)
diff --git a/docs/src/content/v1-migration/zipWith.md b/docs/src/content/v1-migration/zipWith.md
index 601c6d8..dce14b6 100644
--- a/docs/src/content/v1-migration/zipWith.md
+++ b/docs/src/content/v1-migration/zipWith.md
@@ -1,5 +1,9 @@
 # Runtime
 
+The order of parameters in the dataLast invocation has been flipped so that the
+second array to zip with is now the first parameter, and the zipping function is
+now the second param.
+
 The zipping function now takes 2 additional parameters: `index` - The index of
 the current element being processed in array, and `datum` - A 2-tuple of arrays
 the function was called upon (the same signature the callbacks the built-in
@@ -13,6 +17,22 @@ to warn against these issues.
 
 ## Examples
 
+### Param reordered
+
+```ts
+// Was
+pipe(
+  [1, 2, 3],
+  zipWith((a, b) => a + b, [4, 5, 6]),
+);
+
+// Now
+pipe(
+  [1, 2, 3],
+  zipWith([4, 5, 6], (a, b) => a + b),
+);
+```
+
 ### New Params
 
 ```ts
diff --git a/docs/src/lib/v1/transform.ts b/docs/src/lib/v1/transform.ts
index f6255ee..2e48e3e 100644
--- a/docs/src/lib/v1/transform.ts
+++ b/docs/src/lib/v1/transform.ts
@@ -184,8 +184,6 @@ function createCategoriesLookup(
       continue;
     }
 
-    // TODO: We can enforce that only a predefined set of categories is
-    // acceptable and break the build on any unknown categories
     for (const id of children) {
       result.set(id, title);
     }
diff --git a/mapping.md b/mapping.md
index 2dc841d..12ad0b0 100644
--- a/mapping.md
+++ b/mapping.md
@@ -104,6 +104,7 @@ documentation when migrating._
 | [`swapIndices`](https://remedajs.com/docs/#swapIndices)             |                                                                          | [`swap`](https://ramdajs.com/docs/#swap)                           |
 | [`swapProps`](https://remedajs.com/docs/#swapProps)                 |                                                                          | [`swap`](https://ramdajs.com/docs/#swap)                           |
 | [`take`](https://remedajs.com/docs/#take)                           | [`take`](https://lodash.com/docs/4.17.15#take)                           | [`take`](https://ramdajs.com/docs/#take)                           |
+| [`takeLast`](https://remedajs.com/docs/#takeLast)                   | [`takeRight`](https://lodash.com/docs/4.17.15#takeRight)                 | [`takeLast`](https://ramdajs.com/docs/#takeLast)                   |
 | [`takeLastWhile`](https://remedajs.com/docs/#takeLastWhile)         | [`takeRightWhile`](https://lodash.com/docs/4.17.15#takeRightWhile)       | [`takeLastWhile`](https://ramdajs.com/docs/#takeLastWhile)         |
 | [`takeWhile`](https://remedajs.com/docs/#takeWhile)                 | [`takeWhile`](https://lodash.com/docs/4.17.15#takeWhile)                 | [`takeWhile`](https://ramdajs.com/docs/#takeWhile)                 |
 | [`tap`](https://remedajs.com/docs/#tap)                             | [`tap`](https://lodash.com/docs/4.17.15#tap)                             | [`tap`](https://ramdajs.com/docs/#tap)                             |
diff --git a/src/add.ts b/src/add.ts
index a8c5297..ce1b966 100644
--- a/src/add.ts
+++ b/src/add.ts
@@ -14,6 +14,7 @@ import { purry } from "./purry";
  * @dataFirst
  * @category Number
  */
+export function add(value: bigint, addend: bigint): bigint;
 export function add(value: number, addend: number): number;
 
 /**
@@ -29,11 +30,14 @@ export function add(value: number, addend: number): number;
  * @dataLast
  * @category Number
  */
+export function add(addend: bigint): (value: bigint) => bigint;
 export function add(addend: number): (value: number) => number;
 
 export function add(...args: ReadonlyArray<unknown>): unknown {
   return purry(addImplementation, args);
 }
 
+// The implementation only uses `number` types, but that's just because it's
+// hard to tell typescript that both value and addend would be of the same type.
 const addImplementation = (value: number, addend: number): number =>
   value + addend;
diff --git a/src/addProp.ts b/src/addProp.ts
index 9a6692e..d6be098 100644
--- a/src/addProp.ts
+++ b/src/addProp.ts
@@ -15,7 +15,7 @@ import { purry } from "./purry";
  */
 export function addProp<
   T extends Record<PropertyKey, unknown>,
-  K extends string,
+  K extends PropertyKey,
   V,
 >(obj: T, prop: K, value: V): T & { [x in K]: V };
 
@@ -33,7 +33,7 @@ export function addProp<
  */
 export function addProp<
   T extends Record<PropertyKey, unknown>,
-  K extends string,
+  K extends PropertyKey,
   V,
 >(prop: K, value: V): (obj: T) => T & { [x in K]: V };
 
@@ -43,7 +43,7 @@ export function addProp(...args: ReadonlyArray<unknown>): unknown {
 
 const addPropImplementation = <
   T extends Record<PropertyKey, unknown>,
-  K extends string,
+  K extends PropertyKey,
   V,
 >(
   obj: T,
diff --git a/src/difference.ts b/src/difference.ts
index de7ccb3..4cd2c5c 100644
--- a/src/difference.ts
+++ b/src/difference.ts
@@ -1,7 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
-import { lazyIdentityEvaluator } from "./internal/utilityEvaluators";
+import { purryFromLazy } from "./internal/purryFromLazy";
+import { SKIP_ITEM, lazyIdentityEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
 
 /**
  * Excludes the values from `other` array. The output maintains the same order
@@ -44,14 +43,9 @@ export function difference<T>(
 ): (data: ReadonlyArray<T>) => Array<T>;
 
 export function difference(...args: ReadonlyArray<unknown>): unknown {
-  return purry(differenceImplementation, args, lazyImplementation);
+  return purryFromLazy(lazyImplementation, args);
 }
 
-const differenceImplementation = <T>(
-  array: ReadonlyArray<T>,
-  other: ReadonlyArray<T>,
-): Array<T> => reduceLazy(array, lazyImplementation(other));
-
 function lazyImplementation<T>(other: ReadonlyArray<T>): LazyEvaluator<T> {
   if (other.length === 0) {
     return lazyIdentityEvaluator;
@@ -77,6 +71,6 @@ function lazyImplementation<T>(other: ReadonlyArray<T>): LazyEvaluator<T> {
     // copies of it to "account" for so we skip this one and remove it from our
     // ongoing tally.
     remaining.set(value, copies - 1);
-    return { done: false, hasNext: false };
+    return SKIP_ITEM;
   };
 }
diff --git a/src/differenceWith.ts b/src/differenceWith.ts
index 17d8b84..22f49b1 100644
--- a/src/differenceWith.ts
+++ b/src/differenceWith.ts
@@ -1,6 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
+import { purryFromLazy } from "./internal/purryFromLazy";
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
 
 type IsEquals<TFirst, TSecond> = (a: TFirst, b: TSecond) => boolean;
 
@@ -57,15 +57,7 @@ export function differenceWith<TFirst, TSecond>(
 ): (array: ReadonlyArray<TFirst>) => Array<TFirst>;
 
 export function differenceWith(...args: ReadonlyArray<unknown>): unknown {
-  return purry(differenceWithImplementation, args, lazyImplementation);
-}
-
-function differenceWithImplementation<TFirst, TSecond>(
-  array: ReadonlyArray<TFirst>,
-  other: ReadonlyArray<TSecond>,
-  isEquals: IsEquals<TFirst, TSecond>,
-): Array<TFirst> {
-  return reduceLazy(array, lazyImplementation(other, isEquals));
+  return purryFromLazy(lazyImplementation, args);
 }
 
 const lazyImplementation =
@@ -76,4 +68,4 @@ const lazyImplementation =
   (value) =>
     other.every((otherValue) => !isEquals(value, otherValue))
       ? { done: false, hasNext: true, next: value }
-      : { done: false, hasNext: false };
+      : SKIP_ITEM;
diff --git a/src/divide.ts b/src/divide.ts
index d46da07..0d69391 100644
--- a/src/divide.ts
+++ b/src/divide.ts
@@ -13,6 +13,7 @@ import { purry } from "./purry";
  * @dataFirst
  * @category Number
  */
+export function divide(value: bigint, divisor: bigint): bigint;
 export function divide(value: number, divisor: number): number;
 
 /**
@@ -27,11 +28,15 @@ export function divide(value: number, divisor: number): number;
  * @dataLast
  * @category Number
  */
+export function divide(divisor: bigint): (value: bigint) => bigint;
 export function divide(divisor: number): (value: number) => number;
 
 export function divide(...args: ReadonlyArray<unknown>): unknown {
   return purry(divideImplementation, args);
 }
 
+// The implementation only uses `number` types, but that's just because it's
+// hard to tell typescript that both value and divisor would be of the same
+// type.
 const divideImplementation = (value: number, divisor: number): number =>
   value / divisor;
diff --git a/src/drop.ts b/src/drop.ts
index 43252b1..b2f5660 100644
--- a/src/drop.ts
+++ b/src/drop.ts
@@ -1,4 +1,4 @@
-import { reduceLazy } from "./internal/reduceLazy";
+import { SKIP_ITEM, lazyIdentityEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
@@ -35,16 +35,19 @@ export function drop(...args: ReadonlyArray<unknown>): unknown {
   return purry(dropImplementation, args, lazyImplementation);
 }
 
-function dropImplementation<T>(array: ReadonlyArray<T>, n: number): Array<T> {
-  return reduceLazy(array, lazyImplementation(n));
-}
+const dropImplementation = <T>(array: ReadonlyArray<T>, n: number): Array<T> =>
+  n < 0 ? [...array] : array.slice(n);
 
 function lazyImplementation<T>(n: number): LazyEvaluator<T> {
+  if (n <= 0) {
+    return lazyIdentityEvaluator;
+  }
+
   let left = n;
   return (value) => {
     if (left > 0) {
       left -= 1;
-      return { done: false, hasNext: false };
+      return SKIP_ITEM;
     }
     return { done: false, hasNext: true, next: value };
   };
diff --git a/src/dropLast.ts b/src/dropLast.ts
index af29973..fe095c4 100644
--- a/src/dropLast.ts
+++ b/src/dropLast.ts
@@ -31,13 +31,7 @@ export function dropLast(...args: ReadonlyArray<unknown>): unknown {
   return purry(dropLastImplementation, args);
 }
 
-function dropLastImplementation<T>(
+const dropLastImplementation = <T>(
   array: ReadonlyArray<T>,
   n: number,
-): Array<T> {
-  const copy = [...array];
-  if (n > 0) {
-    copy.splice(-n);
-  }
-  return copy;
-}
+): Array<T> => (n >= 0 ? array.slice(0, array.length - n) : [...array]);
diff --git a/src/filter.ts b/src/filter.ts
index 9dbba31..c030ddb 100644
--- a/src/filter.ts
+++ b/src/filter.ts
@@ -1,3 +1,4 @@
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
@@ -70,4 +71,4 @@ const lazyImplementation =
   (value, index, data) =>
     predicate(value, index, data)
       ? { done: false, hasNext: true, next: value }
-      : { done: false, hasNext: false };
+      : SKIP_ITEM;
diff --git a/src/find.ts b/src/find.ts
index 62b1183..f366d62 100644
--- a/src/find.ts
+++ b/src/find.ts
@@ -1,4 +1,5 @@
 import { toSingle } from "./internal/toSingle";
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
@@ -90,4 +91,4 @@ const lazyImplementation =
   (value, index, data) =>
     predicate(value, index, data)
       ? { done: true, hasNext: true, next: value }
-      : { done: false, hasNext: false };
+      : SKIP_ITEM;
diff --git a/src/first.ts b/src/first.ts
index c5c2ab2..fc97073 100644
--- a/src/first.ts
+++ b/src/first.ts
@@ -53,7 +53,7 @@ export function first(...args: ReadonlyArray<unknown>): unknown {
 const firstImplementation = <T>([item]: ReadonlyArray<T>): T | undefined =>
   item;
 
-const lazyImplementation =
-  <T>(): LazyEvaluator<T> =>
-  // eslint-disable-next-line unicorn/consistent-function-scoping -- TODO
-  (value) => ({ done: true, hasNext: true, next: value });
+const lazyImplementation = (): LazyEvaluator => firstLazy;
+
+const firstLazy = <T>(value: T) =>
+  ({ hasNext: true, next: value, done: true }) as const;
diff --git a/src/flat.ts b/src/flat.ts
index 4dbd689..7e776d3 100644
--- a/src/flat.ts
+++ b/src/flat.ts
@@ -1,6 +1,7 @@
 import { type IsNumericLiteral } from "type-fest";
 import { lazyDataLastImpl } from "./internal/lazyDataLastImpl";
 import type { IterableContainer } from "./internal/types";
+import { lazyIdentityEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator, LazyResult } from "./pipe";
 
 // This is obvious and not likely to change, but it makes reading the code a
@@ -113,38 +114,36 @@ export function flat(
   depth?: number,
 ): unknown {
   if (typeof dataOrDepth === "object") {
-    // TODO: Use the built-in `Array.prototype.flat` in V2.
     return flatImplementation(dataOrDepth, depth);
   }
 
   return lazyDataLastImpl(
     flatImplementation,
-    [dataOrDepth, depth],
+    dataOrDepth === undefined ? [] : [dataOrDepth],
     lazyImplementation,
   );
 }
 
-const lazyImplementation = (depth = DEFAULT_DEPTH): LazyEvaluator =>
-  depth <= 0
-    ? lazyIdentity
-    : depth === 1
-      ? lazyShallow
+const flatImplementation = (
+  data: IterableContainer,
+  depth?: number,
+): IterableContainer => (depth === undefined ? data.flat() : data.flat(depth));
+
+const lazyImplementation = (depth?: number): LazyEvaluator =>
+  depth === undefined || depth === 1
+    ? lazyShallow
+    : depth <= 0
+      ? lazyIdentityEvaluator
       : (value) =>
           Array.isArray(value)
             ? {
-                next: flatImplementation(value, depth - 1),
+                next: value.flat(depth - 1),
                 hasNext: true,
                 hasMany: true,
                 done: false,
               }
             : { next: value, hasNext: true, done: false };
 
-// This function is pulled out so that we don't generate a new arrow function
-// each time. It acts as a lazy identity function by wrapping the value with a
-// lazy object.
-const lazyIdentity = <T>(value: T) =>
-  ({ next: value, hasNext: true, done: false }) as const;
-
 // This function is pulled out so that we don't generate a new arrow function
 // each time. Because it doesn't need to run with recursion it could be pulled
 // out from the lazyImplementation and be reused for all invocations.
@@ -152,24 +151,3 @@ const lazyShallow = <T>(value: T): LazyResult<T> =>
   Array.isArray(value)
     ? { next: value, hasNext: true, hasMany: true, done: false }
     : { next: value, hasNext: true, done: false };
-
-function flatImplementation(
-  data: IterableContainer,
-  depth = DEFAULT_DEPTH,
-): IterableContainer {
-  if (depth <= 0) {
-    return [...data];
-  }
-
-  const output: Array<unknown> = [];
-
-  for (const item of data) {
-    if (Array.isArray(item)) {
-      output.push(...flatImplementation(item, depth - 1));
-    } else {
-      output.push(item);
-    }
-  }
-
-  return output;
-}
diff --git a/src/index.ts b/src/index.ts
index 4fcffff..6ad41e6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -121,6 +121,7 @@ export * from "./swapIndices";
 export * from "./swapProps";
 export * from "./take";
 export * from "./takeFirstBy";
+export * from "./takeLast.js";
 export * from "./takeLastWhile";
 export * from "./takeWhile";
 export * from "./tap";
diff --git a/src/internal/purryFromLazy.ts b/src/internal/purryFromLazy.ts
new file mode 100644
index 0000000..ec711e0
--- /dev/null
+++ b/src/internal/purryFromLazy.ts
@@ -0,0 +1,44 @@
+import { pipe, type LazyDefinition, type LazyEvaluator } from "../pipe";
+
+/**
+ * A version of `purry` for cases where the only meaningful implementation is a
+ * lazy one. This is useful for functions that don't have a built-in
+ * implementation already, and that can't be optimized to take advantage of
+ * having the complete array upfront.
+ *
+ * Under the hood the function uses `pipe` to utilize it's built-in lazy logic
+ * and wraps the pipe with the required invocations to allow using the function
+ * outside of pipes too.
+ *
+ * @param lazy - The main lazy implementation, it assumes that data is an
+ * iterable (array-like).
+ * @param args - The arguments passed to the overloaded invocation.
+ * @see purry
+ * @see pipe
+ */
+export function purryFromLazy(
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  lazy: (...args: any) => LazyEvaluator,
+  args: ReadonlyArray<unknown>,
+): unknown {
+  const diff = args.length - lazy.length;
+
+  if (diff === 1) {
+    // dataFirst
+    const [data, ...rest] = args;
+    const lazyDefinition = { lazy, lazyArgs: rest } satisfies LazyDefinition;
+
+    // @ts-expect-error [ts2353] - Pipe expects a function which *might* also have a lazy definition. We are tricking pipe here to take our lazyDefinition without any function attached to it.
+    return pipe(data, lazyDefinition);
+  }
+
+  if (diff === 0) {
+    const lazyDefinition = { lazy, lazyArgs: args } satisfies LazyDefinition;
+
+    // @ts-expect-error [ts2353] - Pipe expects a function which *might* also have a lazy definition. We are tricking pipe here to take our lazyDefinition without any function attached to it.
+    const dataLast = (data: unknown): unknown => pipe(data, lazyDefinition);
+    return Object.assign(dataLast, lazyDefinition);
+  }
+
+  throw new Error("Wrong number of arguments");
+}
diff --git a/src/internal/reduceLazy.ts b/src/internal/reduceLazy.ts
deleted file mode 100644
index 2f935f5..0000000
--- a/src/internal/reduceLazy.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { LazyEvaluator } from "../pipe";
-
-export function reduceLazy<T, K>(
-  array: ReadonlyArray<T>,
-  lazy: LazyEvaluator<T, K>,
-): Array<K> {
-  const out: Array<K> = [];
-
-  // We intentionally use a for loop here instead of reduce for performance reasons. See https://leanylabs.com/blog/js-forEach-map-reduce-vs-for-for_of/ for more info
-  for (const [index, item] of array.entries()) {
-    const result = lazy(item, index, array);
-    if (result.hasMany === true) {
-      out.push(...result.next);
-    } else if (result.hasNext) {
-      out.push(result.next);
-    }
-
-    if (result.done) {
-      break;
-    }
-  }
-
-  return out;
-}
diff --git a/src/internal/utilityEvaluators.ts b/src/internal/utilityEvaluators.ts
index 3606f13..f0299b4 100644
--- a/src/internal/utilityEvaluators.ts
+++ b/src/internal/utilityEvaluators.ts
@@ -2,6 +2,11 @@ import type { LazyResult } from "../pipe";
 
 const EMPTY_PIPE = { done: true, hasNext: false } as const;
 
+/**
+ * A singleton value for skipping an item in a lazy evaluator.
+ */
+export const SKIP_ITEM = { done: false, hasNext: false } as const;
+
 /**
  * A helper evaluator when we want to return an empty result. It memoizes both
  * the result and the evaluator itself to reduce memory usage.
diff --git a/src/intersection.ts b/src/intersection.ts
index 2e1b470..7e9b4e8 100644
--- a/src/intersection.ts
+++ b/src/intersection.ts
@@ -1,7 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
-import { lazyEmptyEvaluator } from "./internal/utilityEvaluators";
+import { purryFromLazy } from "./internal/purryFromLazy";
+import { SKIP_ITEM, lazyEmptyEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
 
 /**
  * Returns a list of elements that exist in both array. The output maintains the
@@ -44,14 +43,9 @@ export function intersection<S>(
 ): <T>(data: ReadonlyArray<T>) => Array<S & T>;
 
 export function intersection(...args: ReadonlyArray<unknown>): unknown {
-  return purry(intersectionImplementation, args, lazyImplementation);
+  return purryFromLazy(lazyImplementation, args);
 }
 
-const intersectionImplementation = <T, S>(
-  data: ReadonlyArray<T>,
-  other: ReadonlyArray<S>,
-): Array<S & T> => reduceLazy(data, lazyImplementation(other));
-
 function lazyImplementation<T, S>(
   other: ReadonlyArray<S>,
 ): LazyEvaluator<T, S & T> {
@@ -72,7 +66,7 @@ function lazyImplementation<T, S>(
     if (copies === undefined || copies === 0) {
       // The item is either not part of the other array or we've "used" enough
       // copies of it so we skip the remaining values.
-      return { done: false, hasNext: false };
+      return SKIP_ITEM;
     }
 
     // The item is equal to an item in the other array and there are still
diff --git a/src/intersectionWith.ts b/src/intersectionWith.ts
index 9a7eb13..8c22b58 100644
--- a/src/intersectionWith.ts
+++ b/src/intersectionWith.ts
@@ -1,6 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
+import { purryFromLazy } from "./internal/purryFromLazy";
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
 
 type Comparator<TFirst, TSecond> = (a: TFirst, b: TSecond) => boolean;
 
@@ -62,15 +62,9 @@ export function intersectionWith<TFirst, TSecond>(
 ): (array: ReadonlyArray<TFirst>) => Array<TFirst>;
 
 export function intersectionWith(...args: ReadonlyArray<unknown>): unknown {
-  return purry(intersectionWithImplementation, args, lazyImplementation);
+  return purryFromLazy(lazyImplementation, args);
 }
 
-const intersectionWithImplementation = <TFirst, TSecond>(
-  array: ReadonlyArray<TFirst>,
-  other: ReadonlyArray<TSecond>,
-  comparator: Comparator<TFirst, TSecond>,
-): Array<TFirst> => reduceLazy(array, lazyImplementation(other, comparator));
-
 const lazyImplementation =
   <TFirst, TSecond>(
     other: ReadonlyArray<TSecond>,
@@ -79,4 +73,4 @@ const lazyImplementation =
   (value) =>
     other.some((otherValue) => comparator(value, otherValue))
       ? { done: false, hasNext: true, next: value }
-      : { done: false, hasNext: false };
+      : SKIP_ITEM;
diff --git a/src/isEmpty.ts b/src/isEmpty.ts
index b1c3ccc..83ce04a 100644
--- a/src/isEmpty.ts
+++ b/src/isEmpty.ts
@@ -1,7 +1,4 @@
 import type { IterableContainer } from "./internal/types";
-import { isArray } from "./isArray";
-import { isObjectType } from "./isObjectType";
-import { isString } from "./isString";
 
 /**
  * A function that checks if the passed parameter is empty.
@@ -37,16 +34,22 @@ export function isEmpty(data: IterableContainer): data is [];
 export function isEmpty<T extends Readonly<Record<PropertyKey, unknown>>>(
   data: T,
 ): data is Record<keyof T, never>;
-export function isEmpty(data: unknown): boolean {
+export function isEmpty(data: object | string | undefined): boolean {
   if (data === undefined) {
     return true;
   }
 
-  if (isArray(data) || isString(data)) {
+  if (typeof data === "string") {
     return data.length === 0;
   }
-  if (isObjectType(data)) {
-    return Object.keys(data).length === 0;
+
+  if (typeof data !== "object") {
+    return false;
   }
-  return false;
+
+  if (Array.isArray(data)) {
+    return data.length === 0;
+  }
+
+  return Object.keys(data).length === 0;
 }
diff --git a/src/mapWithFeedback.ts b/src/mapWithFeedback.ts
index 285f6df..b58dba6 100644
--- a/src/mapWithFeedback.ts
+++ b/src/mapWithFeedback.ts
@@ -1,7 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
+import { purryFromLazy } from "./internal/purryFromLazy";
 import type { IterableContainer, Mapped } from "./internal/types";
 import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
 
 /**
  * Applies a function on each element of the array, using the result of the
@@ -69,20 +68,9 @@ export function mapWithFeedback<T extends IterableContainer, U>(
 ): (data: T) => Mapped<T, U>;
 
 export function mapWithFeedback(...args: ReadonlyArray<unknown>): unknown {
-  return purry(mapWithFeedbackImplementation, args, lazyImplementation);
+  return purryFromLazy(lazyImplementation, args);
 }
 
-const mapWithFeedbackImplementation = <T, U>(
-  data: ReadonlyArray<T>,
-  reducer: (
-    previousValue: U,
-    currentValue: T,
-    index: number,
-    data: ReadonlyArray<T>,
-  ) => U,
-  initialValue: U,
-): Array<U> => reduceLazy(data, lazyImplementation(reducer, initialValue));
-
 const lazyImplementation = <T, U>(
   reducer: (
     previousValue: U,
diff --git a/src/multiply.ts b/src/multiply.ts
index 3b079ce..6236c22 100644
--- a/src/multiply.ts
+++ b/src/multiply.ts
@@ -13,6 +13,7 @@ import { purry } from "./purry";
  * @dataFirst
  * @category Number
  */
+export function multiply(value: bigint, multiplicand: bigint): bigint;
 export function multiply(value: number, multiplicand: number): number;
 
 /**
@@ -27,11 +28,15 @@ export function multiply(value: number, multiplicand: number): number;
  * @dataLast
  * @category Number
  */
+export function multiply(multiplicand: bigint): (value: bigint) => bigint;
 export function multiply(multiplicand: number): (value: number) => number;
 
 export function multiply(...args: ReadonlyArray<unknown>): unknown {
   return purry(multiplyImplementation, args);
 }
 
+// The implementation only uses `number` types, but that's just because it's
+// hard to tell typescript that both value and multiplicand would be of the same
+// type.
 const multiplyImplementation = (value: number, multiplicand: number): number =>
   value * multiplicand;
diff --git a/src/pipe.ts b/src/pipe.ts
index 3984022..feda29e 100644
--- a/src/pipe.ts
+++ b/src/pipe.ts
@@ -1,6 +1,8 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable jsdoc/check-param-names, jsdoc/require-param -- we don't document the op params, it'd be redundant */
 
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
+
 export type LazyEvaluator<T = unknown, R = T> = (
   item: T,
   index: number,
@@ -44,13 +46,17 @@ type LazyFn = (
   items: ReadonlyArray<unknown>,
 ) => LazyResult<unknown>;
 
-type LazyOp = ((input: unknown) => unknown) & {
-  readonly lazy: ((...args: any) => LazyFn) & {
-    readonly single: boolean;
-  };
+type LazyMeta = {
+  readonly single?: boolean;
+};
+
+export type LazyDefinition = {
+  readonly lazy: LazyMeta & ((...args: any) => LazyFn);
   readonly lazyArgs: ReadonlyArray<unknown>;
 };
 
+type LazyOp = LazyDefinition & ((input: unknown) => unknown);
+
 /**
  * Perform left-to-right function composition.
  *
@@ -307,7 +313,7 @@ function processItem(
 
   let currentItem = item;
 
-  let lazyResult: LazyResult<any> = { done: false, hasNext: false };
+  let lazyResult: LazyResult<any> = SKIP_ITEM;
   let isDone = false;
   for (const [operationsIndex, lazyFn] of lazySequence.entries()) {
     const { index, items } = lazyFn;
@@ -352,7 +358,7 @@ function prepareLazyOperation(op: LazyOp): PreparedLazyOperation {
   const { lazy, lazyArgs } = op;
   const fn = lazy(...lazyArgs);
   return Object.assign(fn, {
-    isSingle: lazy.single,
+    isSingle: lazy.single ?? false,
     index: 0,
     items: [] as Array<unknown>,
   });
diff --git a/src/prop.ts b/src/prop.ts
index 0c08d24..0257974 100644
--- a/src/prop.ts
+++ b/src/prop.ts
@@ -31,10 +31,7 @@ export function prop(...args: ReadonlyArray<unknown>): unknown {
   return purry(propImplementation, args);
 }
 
-export function propImplementation<T, K extends keyof T>(
+export const propImplementation = <T, K extends keyof T>(
   data: T,
   key: K,
-): T[K] {
-  const { [key]: value } = data;
-  return value;
-}
+): T[K] => data[key];
diff --git a/src/splitAt.ts b/src/splitAt.ts
index 05040ec..cd3590f 100644
--- a/src/splitAt.ts
+++ b/src/splitAt.ts
@@ -42,7 +42,9 @@ function splitAtImplementation<T>(
   array: ReadonlyArray<T>,
   index: number,
 ): [Array<T>, Array<T>] {
-  const copy = [...array];
-  const tail = copy.splice(index);
-  return [copy, tail];
+  const effectiveIndex = Math.max(
+    Math.min(index < 0 ? array.length + index : index, array.length),
+    0,
+  );
+  return [array.slice(0, effectiveIndex), array.slice(effectiveIndex)];
 }
diff --git a/src/splitWhen.ts b/src/splitWhen.ts
index 3fb0522..aa3d72c 100644
--- a/src/splitWhen.ts
+++ b/src/splitWhen.ts
@@ -1,4 +1,3 @@
-import { splitAt } from "./splitAt";
 import { purry } from "./purry";
 
 /**
@@ -41,11 +40,8 @@ function splitWhenImplementation<T>(
   data: ReadonlyArray<T>,
   predicate: (item: T, index: number, data: ReadonlyArray<T>) => boolean,
 ): [Array<T>, Array<T>] {
-  for (const [index, item] of data.entries()) {
-    if (predicate(item, index, data)) {
-      return splitAt(data, index);
-    }
-  }
-
-  return [[...data], []];
+  const index = data.findIndex(predicate);
+  return index === -1
+    ? [[...data], []]
+    : [data.slice(0, index), data.slice(index)];
 }
diff --git a/src/subtract.ts b/src/subtract.ts
index 1d1bd10..f824e75 100644
--- a/src/subtract.ts
+++ b/src/subtract.ts
@@ -14,6 +14,7 @@ import { purry } from "./purry";
  * @dataFirst
  * @category Number
  */
+export function subtract(value: bigint, subtrahend: bigint): bigint;
 export function subtract(value: number, subtrahend: number): number;
 
 /**
@@ -29,11 +30,15 @@ export function subtract(value: number, subtrahend: number): number;
  * @dataLast
  * @category Number
  */
+export function subtract(subtrahend: bigint): (value: bigint) => bigint;
 export function subtract(subtrahend: number): (value: number) => number;
 
 export function subtract(...args: ReadonlyArray<unknown>): unknown {
   return purry(subtractImplementation, args);
 }
 
+// The implementation only uses `number` types, but that's just because it's
+// hard to tell typescript that both value and subtrahend would be of the same
+// type.
 const subtractImplementation = (value: number, subtrahend: number): number =>
   value - subtrahend;
diff --git a/src/take.ts b/src/take.ts
index e70935c..6d6530c 100644
--- a/src/take.ts
+++ b/src/take.ts
@@ -1,4 +1,4 @@
-import { reduceLazy } from "./internal/reduceLazy";
+import { lazyEmptyEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
@@ -36,11 +36,11 @@ export function take(...args: ReadonlyArray<unknown>): unknown {
 }
 
 const takeImplementation = <T>(array: ReadonlyArray<T>, n: number): Array<T> =>
-  reduceLazy(array, lazyImplementation(n));
+  n < 0 ? [] : array.slice(0, n);
 
 function lazyImplementation<T>(n: number): LazyEvaluator<T> {
   if (n <= 0) {
-    return emptyPipe;
+    return lazyEmptyEvaluator;
   }
 
   let remaining = n;
@@ -49,9 +49,3 @@ function lazyImplementation<T>(n: number): LazyEvaluator<T> {
     return { done: remaining <= 0, hasNext: true, next: value };
   };
 }
-
-// We optimize the trivial case by memoizing both the returned object and the
-// function that returns it so that none of them need to be recreated on every
-// invocation.
-const LAZY_DONE = { done: true, hasNext: false } as const;
-const emptyPipe = (): typeof LAZY_DONE => LAZY_DONE;
diff --git a/src/takeLast.ts b/src/takeLast.ts
new file mode 100644
index 0000000..2b484b5
--- /dev/null
+++ b/src/takeLast.ts
@@ -0,0 +1,37 @@
+import { purry } from "./purry";
+
+/**
+ * Takes the last `n` elements from the `array`.
+ *
+ * @param array - The target array.
+ * @param n - The number of elements to take.
+ * @signature
+ *    R.takeLast(array, n)
+ * @example
+ *    R.takeLast([1, 2, 3, 4, 5], 2) // => [4, 5]
+ * @dataFirst
+ * @category Array
+ */
+export function takeLast<T>(array: ReadonlyArray<T>, n: number): Array<T>;
+
+/**
+ * Take the last `n` elements from the `array`.
+ *
+ * @param n - The number of elements to take.
+ * @signature
+ *    R.takeLast(n)(array)
+ * @example
+ *    R.takeLast(2)([1, 2, 3, 4, 5]) // => [4, 5]
+ * @dataLast
+ * @category Array
+ */
+export function takeLast<T>(n: number): (array: ReadonlyArray<T>) => Array<T>;
+
+export function takeLast(...args: ReadonlyArray<unknown>): unknown {
+  return purry(takeLastImplementation, args);
+}
+
+const takeLastImplementation = <T>(
+  array: ReadonlyArray<T>,
+  n: number,
+): Array<T> => (n >= 0 ? array.slice(array.length - n) : []);
diff --git a/src/unique.ts b/src/unique.ts
index 4e389ee..f0b272e 100644
--- a/src/unique.ts
+++ b/src/unique.ts
@@ -1,6 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
+import { purryFromLazy } from "./internal/purryFromLazy";
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
 
 /**
  * Returns a new array containing only one copy of each element in the original
@@ -36,17 +36,14 @@ export function unique<T>(array: ReadonlyArray<T>): Array<T>;
 export function unique<T>(): (array: ReadonlyArray<T>) => Array<T>;
 
 export function unique(...args: ReadonlyArray<unknown>): unknown {
-  return purry(uniqueImplementation, args, lazyImplementation);
+  return purryFromLazy(lazyImplementation, args);
 }
 
-const uniqueImplementation = <T>(array: ReadonlyArray<T>): Array<T> =>
-  reduceLazy(array, lazyImplementation());
-
 function lazyImplementation<T>(): LazyEvaluator<T> {
   const set = new Set<T>();
   return (value) => {
     if (set.has(value)) {
-      return { done: false, hasNext: false };
+      return SKIP_ITEM;
     }
     set.add(value);
     return { done: false, hasNext: true, next: value };
diff --git a/src/uniqueBy.ts b/src/uniqueBy.ts
index 75d12f6..f7e77f5 100644
--- a/src/uniqueBy.ts
+++ b/src/uniqueBy.ts
@@ -1,6 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
+import { purryFromLazy } from "./internal/purryFromLazy";
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
 
 /**
  * Returns a new array containing only one copy of each element in the original
@@ -46,14 +46,9 @@ export function uniqueBy<T, K>(
 ): (data: ReadonlyArray<T>) => Array<T>;
 
 export function uniqueBy(...args: ReadonlyArray<unknown>): unknown {
-  return purry(uniqueByImplementation, args, lazyImplementation);
+  return purryFromLazy(lazyImplementation, args);
 }
 
-const uniqueByImplementation = <T, K>(
-  data: ReadonlyArray<T>,
-  keyFunction: (item: T, index: number, data: ReadonlyArray<T>) => K,
-): Array<T> => reduceLazy(data, lazyImplementation(keyFunction));
-
 function lazyImplementation<T, K>(
   keyFunction: (item: T, index: number, data: ReadonlyArray<T>) => K,
 ): LazyEvaluator<T> {
@@ -61,7 +56,7 @@ function lazyImplementation<T, K>(
   return (value, index, data) => {
     const key = keyFunction(value, index, data);
     if (set.has(key)) {
-      return { done: false, hasNext: false };
+      return SKIP_ITEM;
     }
 
     set.add(key);
diff --git a/src/uniqueWith.ts b/src/uniqueWith.ts
index 7ddc4fb..8899bc1 100644
--- a/src/uniqueWith.ts
+++ b/src/uniqueWith.ts
@@ -1,6 +1,6 @@
-import { reduceLazy } from "./internal/reduceLazy";
-import type { LazyEvaluator } from "./pipe";
-import { purry } from "./purry";
+import { purryFromLazy } from "./internal/purryFromLazy";
+import { SKIP_ITEM } from "./internal/utilityEvaluators";
+import { type LazyEvaluator } from "./pipe";
 
 type IsEquals<T> = (a: T, b: T) => boolean;
 
@@ -48,17 +48,12 @@ export function uniqueWith<T>(
 ): (data: ReadonlyArray<T>) => Array<T>;
 
 export function uniqueWith(...args: ReadonlyArray<unknown>): unknown {
-  return purry(uniqueWithImplementation, args, lazyImplementation);
+  return purryFromLazy(lazyImplementation, args);
 }
 
-const uniqueWithImplementation = <T>(
-  data: ReadonlyArray<T>,
-  isEquals: IsEquals<T>,
-): Array<T> => reduceLazy(data, lazyImplementation(isEquals));
-
 const lazyImplementation =
   <T>(isEquals: IsEquals<T>): LazyEvaluator<T> =>
   (value, index, data) =>
     data.findIndex((otherValue) => isEquals(value, otherValue)) === index
       ? { done: false, hasNext: true, next: value }
-      : { done: false, hasNext: false };
+      : SKIP_ITEM;
diff --git a/src/zip.ts b/src/zip.ts
index 5c20e03..33559f2 100644
--- a/src/zip.ts
+++ b/src/zip.ts
@@ -1,4 +1,5 @@
 import type { IterableContainer } from "./internal/types";
+import { type LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
 type Zipped<Left extends IterableContainer, Right extends IterableContainer> =
@@ -33,6 +34,7 @@ type Zipped<Left extends IterableContainer, Right extends IterableContainer> =
  * @example
  *   R.zip([1, 2], ['a', 'b']) // => [[1, 'a'], [2, 'b']]
  * @dataFirst
+ * @pipeable
  * @category Array
  */
 export function zip<F extends IterableContainer, S extends IterableContainer>(
@@ -51,6 +53,7 @@ export function zip<F extends IterableContainer, S extends IterableContainer>(
  * @example
  *   R.zip(['a', 'b'])([1, 2]) // => [[1, 'a'], [2, 'b']]
  * @dataLast
+ * @pipeable
  * @category Array
  */
 export function zip<S extends IterableContainer>(
@@ -58,21 +61,26 @@ export function zip<S extends IterableContainer>(
 ): <F extends IterableContainer>(first: F) => Zipped<F, S>;
 
 export function zip(...args: ReadonlyArray<unknown>): unknown {
-  return purry(zipImplementation, args);
+  return purry(zipImplementation, args, lazyImplementation);
 }
 
-function zipImplementation<
+const zipImplementation = <
   F extends IterableContainer,
   S extends IterableContainer,
->(first: F, second: S): Zipped<F, S> {
-  const resultLength =
-    first.length > second.length ? second.length : first.length;
-
-  const result: Array<[F[number], S[number]]> = [];
-
-  for (let i = 0; i < resultLength; i++) {
-    result.push([first[i], second[i]]);
-  }
+>(
+  first: F,
+  second: S,
+): Zipped<F, S> =>
+  (first.length < second.length
+    ? first.map((item, index) => [item, second[index]])
+    : second.map((item, index) => [first[index], item])) as Zipped<F, S>;
 
-  return result as Zipped<F, S>;
-}
+const lazyImplementation =
+  <F extends IterableContainer, S extends IterableContainer>(
+    second: S,
+  ): LazyEvaluator<F[number], [F[number], S[number]]> =>
+  (value, index) => ({
+    hasNext: true,
+    next: [value, second[index]],
+    done: index >= second.length - 1,
+  });
diff --git a/src/zipWith.ts b/src/zipWith.ts
index 6902cf4..7d6eede 100644
--- a/src/zipWith.ts
+++ b/src/zipWith.ts
@@ -1,75 +1,95 @@
-type ZippingFunction<T1 = unknown, T2 = unknown, Value = unknown> = (
-  first: T1,
-  second: T2,
+import { type IterableContainer } from "./internal/types";
+import { type LazyEvaluator } from "./pipe";
+import { lazyDataLastImpl } from "./internal/lazyDataLastImpl";
+
+type ZippingFunction<
+  T1 extends IterableContainer = IterableContainer,
+  T2 extends IterableContainer = IterableContainer,
+  Value = unknown,
+> = (
+  first: T1[number],
+  second: T2[number],
   index: number,
-  data: readonly [first: ReadonlyArray<T1>, second: ReadonlyArray<T2>],
+  data: readonly [first: T1, second: T2],
 ) => Value;
 
 /**
  * Creates a new list from two supplied lists by calling the supplied function
  * with the same-positioned element from each list.
  *
- * @param first - The first input list.
- * @param second - The second input list.
  * @param fn - The function applied to each position of the list.
  * @signature
- *   R.zipWith(first, second, fn)
+ *   R.zipWith(fn)(first, second)
  * @example
- *   R.zipWith(['1', '2', '3'], ['a', 'b', 'c'], (a, b) => a + b) // => ['1a', '2b', '3c']
- * @dataFirst
+ *   R.zipWith((a: string, b: string) => a + b)(['1', '2', '3'], ['a', 'b', 'c']) // => ['1a', '2b', '3c']
  * @category Array
  */
-export function zipWith<T1, T2, Value>(
-  first: ReadonlyArray<T1>,
-  second: ReadonlyArray<T2>,
-  fn: ZippingFunction<T1, T2, Value>,
-): Array<Value>;
+export function zipWith<TItem1, TItem2, Value>(
+  fn: ZippingFunction<ReadonlyArray<TItem1>, ReadonlyArray<TItem2>, Value>,
+): <T1 extends IterableContainer<TItem1>, T2 extends IterableContainer<TItem2>>(
+  first: T1,
+  second: T2,
+) => Array<Value>;
 
 /**
  * Creates a new list from two supplied lists by calling the supplied function
  * with the same-positioned element from each list.
  *
+ * @param second - The second input list.
  * @param fn - The function applied to each position of the list.
  * @signature
- *   R.zipWith(fn)(first, second)
+ *   R.zipWith(second, fn)(first)
  * @example
- *   R.zipWith((a, b) => a + b)(['1', '2', '3'], ['a', 'b', 'c']) // => ['1a', '2b', '3c']
+ *   R.pipe(['1', '2', '3'], R.zipWith(['a', 'b', 'c'], (a, b) => a + b)) // => ['1a', '2b', '3c']
  * @dataLast
+ * @pipeable
  * @category Array
  */
-export function zipWith<T1, T2, Value>(
-  fn: ZippingFunction<T1, T2, Value>,
-): (first: ReadonlyArray<T1>, second: ReadonlyArray<T2>) => Array<Value>;
+export function zipWith<
+  T1 extends IterableContainer,
+  T2 extends IterableContainer,
+  Value,
+>(second: T2, fn: ZippingFunction<T1, T2, Value>): (first: T1) => Array<Value>;
 
 /**
  * Creates a new list from two supplied lists by calling the supplied function
  * with the same-positioned element from each list.
  *
- * @param fn - The function applied to each position of the list.
+ * @param first - The first input list.
  * @param second - The second input list.
+ * @param fn - The function applied to each position of the list.
  * @signature
- *   R.zipWith(fn)(first, second)
+ *   R.zipWith(first, second, fn)
  * @example
- *   R.zipWith((a, b) => a + b, ['a', 'b', 'c'])(['1', '2', '3']) // => ['1a', '2b', '3c']
- * @dataLast
+ *   R.zipWith(['1', '2', '3'], ['a', 'b', 'c'], (a, b) => a + b) // => ['1a', '2b', '3c']
+ * @dataFirst
+ * @pipeable
  * @category Array
  */
-export function zipWith<T1, T2, Value>(
-  fn: ZippingFunction<T1, T2, Value>,
-  second: ReadonlyArray<T2>,
-): (first: ReadonlyArray<T1>) => Array<Value>;
+export function zipWith<
+  T1 extends IterableContainer,
+  T2 extends IterableContainer,
+  Value,
+>(first: T1, second: T2, fn: ZippingFunction<T1, T2, Value>): Array<Value>;
 
 export function zipWith(
-  arg0: ReadonlyArray<unknown> | ZippingFunction,
-  arg1?: ReadonlyArray<unknown>,
+  arg0: IterableContainer | ZippingFunction,
+  arg1?: IterableContainer | ZippingFunction,
   arg2?: ZippingFunction,
 ): unknown {
   if (typeof arg0 === "function") {
+    // Both datum's last
+    return (data1: IterableContainer, data2: IterableContainer) =>
+      zipWithImplementation(data1, data2, arg0);
+  }
+
+  if (typeof arg1 === "function") {
     // dataLast
-    return arg1 === undefined
-      ? (f: ReadonlyArray<unknown>, s: ReadonlyArray<unknown>) =>
-          zipWithImplementation(f, s, arg0)
-      : (f: ReadonlyArray<unknown>) => zipWithImplementation(f, arg1, arg0);
+    return lazyDataLastImpl(
+      zipWithImplementation,
+      [arg0, arg1],
+      lazyImplementation,
+    );
   }
 
   // dataFirst. Notice that we assert that the arguments are defined to reduce
@@ -79,19 +99,24 @@ export function zipWith(
   return zipWithImplementation(arg0, arg1!, arg2!);
 }
 
-function zipWithImplementation<T1, T2, Value>(
-  first: ReadonlyArray<T1>,
-  second: ReadonlyArray<T2>,
-  fn: ZippingFunction<T1, T2, Value>,
-): Array<Value> {
+function zipWithImplementation<
+  T1 extends IterableContainer,
+  T2 extends IterableContainer,
+  Value,
+>(first: T1, second: T2, fn: ZippingFunction<T1, T2, Value>): Array<Value> {
   const datum = [first, second] as const;
-
-  const resultLength =
-    first.length > second.length ? second.length : first.length;
-  const result = [];
-  for (let i = 0; i < resultLength; i++) {
-    result.push(fn(first[i]!, second[i]!, i, datum));
-  }
-
-  return result;
+  return first.length < second.length
+    ? first.map((item, index) => fn(item, second[index], index, datum))
+    : second.map((item, index) => fn(first[index], item, index, datum));
 }
+
+const lazyImplementation =
+  <T1, T2 extends IterableContainer, Value>(
+    second: T2,
+    fn: ZippingFunction<ReadonlyArray<T1>, T2, Value>,
+  ): LazyEvaluator<T1, Value> =>
+  (value, index, data) => ({
+    next: fn(value, second[index], index, [data, second]),
+    hasNext: true,
+    done: index >= second.length - 1,
+  });


Test patch:
diff --git a/src/drop.test.ts b/src/drop.test.ts
index e31703f..db3c71e 100644
--- a/src/drop.test.ts
+++ b/src/drop.test.ts
@@ -1,37 +1,56 @@
-import { createLazyInvocationCounter } from "../test/lazyInvocationCounter";
 import { drop } from "./drop";
+import { identity } from "./identity";
+import { map } from "./map";
 import { pipe } from "./pipe";
 import { take } from "./take";
 
-const array = [1, 2, 3, 4, 5] as const;
-const expected = [3, 4, 5];
+describe("runtime", () => {
+  describe("data first", () => {
+    it("works on regular inputs", () => {
+      expect(drop([1, 2, 3, 4, 5], 2)).toStrictEqual([3, 4, 5]);
+    });
 
-describe("data first", () => {
-  test("should drop", () => {
-    expect(drop(array, 2)).toEqual(expected);
-  });
+    it("works trivially on empty arrays", () => {
+      expect(drop([], 2)).toStrictEqual([]);
+    });
 
-  test("should not drop", () => {
-    expect(drop(array, 0)).toEqual(array);
-    expect(drop(array, -0)).toEqual(array);
-    expect(drop(array, -1)).toEqual(array);
-    expect(drop(array, Number.NaN)).toEqual(array);
-  });
+    it("works trivially with negative numbers", () => {
+      expect(drop([1, 2, 3, 4, 5], -1)).toStrictEqual([1, 2, 3, 4, 5]);
+    });
 
-  test("should return a new array even if there was no drop", () => {
-    expect(drop(array, 0)).not.toBe(array);
-  });
-});
+    it("works when dropping more than the length of the array", () => {
+      expect(drop([1, 2, 3, 4, 5], 10)).toStrictEqual([]);
+    });
 
-describe("data last", () => {
-  test("drop", () => {
-    const result = pipe(array, drop(2));
-    expect(result).toEqual(expected);
+    test("returns a shallow clone when no items are dropped", () => {
+      const data = [1, 2, 3, 4, 5];
+      const result = drop(data, 0);
+      expect(result).toStrictEqual([1, 2, 3, 4, 5]);
+      expect(result).not.toBe(data);
+    });
   });
-  test("drop with take", () => {
-    const counter = createLazyInvocationCounter();
-    const result = pipe(array, counter.fn(), drop(2), take(2));
-    expect(counter.count).toHaveBeenCalledTimes(4);
-    expect(result).toEqual([3, 4]);
+
+  describe("data last", () => {
+    it("works on regular inputs", () => {
+      expect(pipe([1, 2, 3, 4, 5], drop(2))).toStrictEqual([3, 4, 5]);
+    });
+
+    it("works trivially on empty arrays", () => {
+      expect(pipe([], drop(2))).toStrictEqual([]);
+    });
+
+    it("works trivially with negative numbers", () => {
+      expect(pipe([1, 2, 3, 4, 5], drop(-1))).toStrictEqual([1, 2, 3, 4, 5]);
+    });
+
+    it("works when dropping more than the length of the array", () => {
+      expect(pipe([1, 2, 3, 4, 5], drop(10))).toStrictEqual([]);
+    });
+
+    test("lazy impl", () => {
+      const mockFunc = vi.fn(identity());
+      pipe([1, 2, 3, 4, 5], map(mockFunc), drop(2), take(2));
+      expect(mockFunc).toHaveBeenCalledTimes(4);
+    });
   });
 });
diff --git a/src/dropLast.test.ts b/src/dropLast.test.ts
index 36ce39a..d9d7e2b 100644
--- a/src/dropLast.test.ts
+++ b/src/dropLast.test.ts
@@ -1,18 +1,19 @@
 import { dropLast } from "./dropLast";
 
-const arr = [1, 2, 3, 4, 5] as const;
-
 test("should drop last", () => {
-  expect(dropLast(arr, 2)).toEqual([1, 2, 3]);
+  expect(dropLast([1, 2, 3, 4, 5], 2)).toStrictEqual([1, 2, 3]);
 });
 
 test("should not drop last", () => {
-  expect(dropLast(arr, 0)).toEqual(arr);
-  expect(dropLast(arr, -0)).toEqual(arr);
-  expect(dropLast(arr, -1)).toEqual(arr);
-  expect(dropLast(arr, Number.NaN)).toEqual(arr);
+  expect(dropLast([1, 2, 3, 4, 5], 0)).toStrictEqual([1, 2, 3, 4, 5]);
+  expect(dropLast([1, 2, 3, 4, 5], -0)).toStrictEqual([1, 2, 3, 4, 5]);
+  expect(dropLast([1, 2, 3, 4, 5], -1)).toStrictEqual([1, 2, 3, 4, 5]);
+  expect(dropLast([1, 2, 3, 4, 5], Number.NaN)).toStrictEqual([1, 2, 3, 4, 5]);
 });
 
 test("should return a new array even if there was no drop", () => {
-  expect(dropLast(arr, 0)).not.toBe(arr);
+  const data = [1, 2, 3, 4, 5];
+  const result = dropLast(data, 0);
+  expect(result).not.toBe(data);
+  expect(result).toStrictEqual(data);
 });
diff --git a/src/internal/purryFromLazy.test.ts b/src/internal/purryFromLazy.test.ts
new file mode 100644
index 0000000..954dec4
--- /dev/null
+++ b/src/internal/purryFromLazy.test.ts
@@ -0,0 +1,29 @@
+/* eslint-disable unicorn/consistent-function-scoping, @typescript-eslint/explicit-function-return-type --
+ * purry is all about functions, so we need to turn these off to make it easy
+ * to write the tests.
+ */
+
+import { type LazyEvaluator } from "../pipe";
+import { purryFromLazy } from "./purryFromLazy";
+
+describe("runtime", () => {
+  test("throws on wrong number of arguments", () => {
+    const zeroArgsLazyImpl = (): LazyEvaluator => () =>
+      ({ done: true, hasNext: false }) as const;
+
+    const zeroArgsPurried = (...args: ReadonlyArray<unknown>) =>
+      purryFromLazy(zeroArgsLazyImpl, args);
+
+    expect(() =>
+      zeroArgsPurried(
+        // The first argument to the lazy purried function will always be an
+        // array.
+        ["hello"],
+        // But from the second param and onward the params belong to the lazy
+        // impl. Because our lazy impl takes 0 args, this extra param should
+        // throw.
+        "world",
+      ),
+    ).toThrowError("Wrong number of arguments");
+  });
+});
diff --git a/src/internal/reduceLazy.test.ts b/src/internal/reduceLazy.test.ts
deleted file mode 100644
index b440ec0..0000000
--- a/src/internal/reduceLazy.test.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { reduceLazy } from "./reduceLazy";
-
-// _reduceLazy is tested via the functions that call it, but those tests lack
-// several edge cases which need to be tested on their own.
-describe("generic tests", () => {
-  it("handles hasMany", () => {
-    expect(
-      reduceLazy([1, 2, 3], (value) => ({
-        hasNext: true,
-        hasMany: true,
-        next: [value, value, value],
-        done: false,
-      })),
-    ).toStrictEqual([1, 1, 1, 2, 2, 2, 3, 3, 3]);
-  });
-});
diff --git a/src/isEmpty.test.ts b/src/isEmpty.test.ts
index e280adb..558fe9c 100644
--- a/src/isEmpty.test.ts
+++ b/src/isEmpty.test.ts
@@ -1,6 +1,6 @@
 import { isEmpty } from "./isEmpty";
 
-describe("isEmpty", () => {
+describe("runtime", () => {
   test("returns true for an empty array", () => {
     expect(isEmpty([])).toBe(true);
   });
@@ -27,7 +27,9 @@ describe("isEmpty", () => {
   test("returns true for undefined", () => {
     expect(isEmpty(undefined)).toBe(true);
   });
+});
 
+describe("typing", () => {
   test("does not accept invalid input types", () => {
     // @ts-expect-error [ts2769] number is not a valid input type
     isEmpty(2);
@@ -36,7 +38,7 @@ describe("isEmpty", () => {
     isEmpty(false);
 
     // @ts-expect-error [ts2769] null is not a valid input type
-    isEmpty(null);
+    expect(() => isEmpty(null)).toThrow();
 
     // @ts-expect-error [ts2769] undefined is only allowed with strings
     isEmpty([] as ReadonlyArray<string> | undefined);
@@ -44,93 +46,93 @@ describe("isEmpty", () => {
     // @ts-expect-error [ts2769] undefined is only allowed with strings
     isEmpty({} as Record<string, string> | undefined);
   });
-});
-
-describe("strings are narrowed correctly", () => {
-  test("just undefined", () => {
-    const data = undefined;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<undefined>();
-    }
-  });
-
-  test("just string", () => {
-    const data = "" as string;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<"">();
-    }
-  });
-
-  test("just EMPTY string", () => {
-    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
-    const data = "" as const;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<"">();
-    }
-  });
-
-  test("string or undefined", () => {
-    const data = undefined as string | undefined;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<"" | undefined>();
-    }
-  });
-
-  test("string literals that CANT be empty or undefined", () => {
-    const data = "cat" as "cat" | "dog";
-    if (isEmpty(data)) {
-      // unreachable
-      expectTypeOf(data).toEqualTypeOf<never>();
-    }
-  });
-
-  test("string literals that CAN be empty", () => {
-    const data = "cat" as "" | "cat" | "dog";
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<"">();
-    }
-  });
-
-  test("string literals that CAN be undefined", () => {
-    const data = "cat" as "cat" | "dog" | undefined;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<undefined>();
-    }
-  });
-
-  test("string literals that CAN be undefined or empty", () => {
-    const data = "cat" as "" | "cat" | "dog" | undefined;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<"" | undefined>();
-    }
-  });
-
-  test("string templates that CANT be empty or undefined", () => {
-    const data = "prefix_0" as `prefix_${number}`;
-    if (isEmpty(data)) {
-      // unreachable
-      expectTypeOf(data).toEqualTypeOf<never>();
-    }
-  });
-
-  test("string templates that CAN be empty", () => {
-    const data = "" as "" | `prefix_${number}`;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<"">();
-    }
-  });
-
-  test("string templates that CAN be undefined", () => {
-    const data = "prefix_0" as `prefix_${number}` | undefined;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<undefined>();
-    }
-  });
 
-  test("string templates that CAN be undefined or empty", () => {
-    const data = "prefix_0" as "" | `prefix_${number}` | undefined;
-    if (isEmpty(data)) {
-      expectTypeOf(data).toEqualTypeOf<"" | undefined>();
-    }
+  describe("strings are narrowed correctly", () => {
+    test("just undefined", () => {
+      const data = undefined;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<undefined>();
+      }
+    });
+
+    test("just string", () => {
+      const data = "" as string;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<"">();
+      }
+    });
+
+    test("just EMPTY string", () => {
+      // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+      const data = "" as const;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<"">();
+      }
+    });
+
+    test("string or undefined", () => {
+      const data = undefined as string | undefined;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<"" | undefined>();
+      }
+    });
+
+    test("string literals that CANT be empty or undefined", () => {
+      const data = "cat" as "cat" | "dog";
+      if (isEmpty(data)) {
+        // unreachable
+        expectTypeOf(data).toEqualTypeOf<never>();
+      }
+    });
+
+    test("string literals that CAN be empty", () => {
+      const data = "cat" as "" | "cat" | "dog";
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<"">();
+      }
+    });
+
+    test("string literals that CAN be undefined", () => {
+      const data = "cat" as "cat" | "dog" | undefined;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<undefined>();
+      }
+    });
+
+    test("string literals that CAN be undefined or empty", () => {
+      const data = "cat" as "" | "cat" | "dog" | undefined;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<"" | undefined>();
+      }
+    });
+
+    test("string templates that CANT be empty or undefined", () => {
+      const data = "prefix_0" as `prefix_${number}`;
+      if (isEmpty(data)) {
+        // unreachable
+        expectTypeOf(data).toEqualTypeOf<never>();
+      }
+    });
+
+    test("string templates that CAN be empty", () => {
+      const data = "" as "" | `prefix_${number}`;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<"">();
+      }
+    });
+
+    test("string templates that CAN be undefined", () => {
+      const data = "prefix_0" as `prefix_${number}` | undefined;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<undefined>();
+      }
+    });
+
+    test("string templates that CAN be undefined or empty", () => {
+      const data = "prefix_0" as "" | `prefix_${number}` | undefined;
+      if (isEmpty(data)) {
+        expectTypeOf(data).toEqualTypeOf<"" | undefined>();
+      }
+    });
   });
 });
diff --git a/src/take.test.ts b/src/take.test.ts
index 591a16b..591e2a9 100644
--- a/src/take.test.ts
+++ b/src/take.test.ts
@@ -1,18 +1,55 @@
+import { identity } from "./identity";
+import { map } from "./map";
 import { pipe } from "./pipe";
 import { take } from "./take";
 
-describe("data_first", () => {
-  it("take", () => {
-    expect(take([1, 2, 3, 4, 3, 2, 1], 3)).toEqual([1, 2, 3]);
-  });
-});
+describe("runtime", () => {
+  describe("data first", () => {
+    it("works on regular inputs", () => {
+      expect(take([1, 2, 3, 4, 5], 2)).toStrictEqual([1, 2]);
+    });
+
+    it("works trivially on empty arrays", () => {
+      expect(take([], 2)).toStrictEqual([]);
+    });
+
+    it("works trivially with negative numbers", () => {
+      expect(take([1, 2, 3, 4, 5], -1)).toStrictEqual([]);
+    });
+
+    it("works when taking more than the length of the array", () => {
+      expect(take([1, 2, 3, 4, 5], 10)).toStrictEqual([1, 2, 3, 4, 5]);
+    });
 
-describe("data_last", () => {
-  it("take", () => {
-    expect(take(3)([1, 2, 3, 4, 3, 2, 1])).toEqual([1, 2, 3]);
+    test("returns a shallow clone when all items are taken", () => {
+      const data = [1, 2, 3, 4, 5];
+      const result = take(data, 5);
+      expect(result).toStrictEqual([1, 2, 3, 4, 5]);
+      expect(result).not.toBe(data);
+    });
   });
 
-  it("lazy evaluation of trivial n=1", () => {
-    expect(pipe([1, 2, 3, 4, 3, 2, 1], take(0))).toEqual([]);
+  describe("data last", () => {
+    it("works on regular inputs", () => {
+      expect(pipe([1, 2, 3, 4, 5], take(2))).toStrictEqual([1, 2]);
+    });
+
+    it("works trivially on empty arrays", () => {
+      expect(pipe([], take(2))).toStrictEqual([]);
+    });
+
+    it("works trivially with negative numbers", () => {
+      expect(pipe([1, 2, 3, 4, 5], take(-1))).toStrictEqual([]);
+    });
+
+    it("works when taking more than the length of the array", () => {
+      expect(pipe([1, 2, 3, 4, 5], take(10))).toStrictEqual([1, 2, 3, 4, 5]);
+    });
+
+    test("lazy implementation", () => {
+      const mockFunc = vi.fn(identity());
+      pipe([1, 2, 3, 4, 5], map(mockFunc), take(2));
+      expect(mockFunc).toHaveBeenCalledTimes(2);
+    });
   });
 });
diff --git a/src/takeLast.test.ts b/src/takeLast.test.ts
new file mode 100644
index 0000000..1350b34
--- /dev/null
+++ b/src/takeLast.test.ts
@@ -0,0 +1,19 @@
+import { takeLast } from "./takeLast";
+
+test("should take last", () => {
+  expect(takeLast([1, 2, 3, 4, 5], 2)).toStrictEqual([4, 5]);
+});
+
+test("should not take last", () => {
+  expect(takeLast([1, 2, 3, 4, 5], 0)).toStrictEqual([]);
+  expect(takeLast([1, 2, 3, 4, 5], -0)).toStrictEqual([]);
+  expect(takeLast([1, 2, 3, 4, 5], -1)).toStrictEqual([]);
+  expect(takeLast([1, 2, 3, 4, 5], Number.NaN)).toStrictEqual([]);
+});
+
+test("should return a new array even if everything was taken", () => {
+  const data = [1, 2, 3, 4, 5];
+  const result = takeLast(data, data.length);
+  expect(result).not.toBe(data);
+  expect(result).toStrictEqual(data);
+});
diff --git a/src/zip.test.ts b/src/zip.test.ts
index 7cf3976..d5592e4 100644
--- a/src/zip.test.ts
+++ b/src/zip.test.ts
@@ -1,10 +1,13 @@
+import { first } from "./first";
+import { identity } from "./identity";
+import { map } from "./map";
 import { pipe } from "./pipe";
 import { zip } from "./zip";
 
 describe("runtime", () => {
   describe("dataFirst", () => {
     test("should zip", () => {
-      expect(zip([1, 2, 3], ["a", "b", "c"])).toEqual([
+      expect(zip([1, 2, 3], ["a", "b", "c"])).toStrictEqual([
         [1, "a"],
         [2, "b"],
         [3, "c"],
@@ -12,14 +15,14 @@ describe("runtime", () => {
     });
 
     test("should truncate to shorter second", () => {
-      expect(zip([1, 2, 3], ["a", "b"])).toEqual([
+      expect(zip([1, 2, 3], ["a", "b"])).toStrictEqual([
         [1, "a"],
         [2, "b"],
       ]);
     });
 
     test("should truncate to shorter first", () => {
-      expect(zip([1, 2], ["a", "b", "c"])).toEqual([
+      expect(zip([1, 2], ["a", "b", "c"])).toStrictEqual([
         [1, "a"],
         [2, "b"],
       ]);
@@ -28,7 +31,7 @@ describe("runtime", () => {
 
   describe("dataLast", () => {
     test("should zip", () => {
-      expect(zip(["a", "b", "c"])([1, 2, 3])).toEqual([
+      expect(pipe([1, 2, 3], zip(["a", "b", "c"]))).toStrictEqual([
         [1, "a"],
         [2, "b"],
         [3, "c"],
@@ -36,18 +39,24 @@ describe("runtime", () => {
     });
 
     test("should truncate to shorter second", () => {
-      expect(zip(["a", "b"])([1, 2, 3])).toEqual([
+      expect(pipe([1, 2, 3], zip(["a", "b"]))).toStrictEqual([
         [1, "a"],
         [2, "b"],
       ]);
     });
 
     test("should truncate to shorter first", () => {
-      expect(zip(["a", "b", "c"])([1, 2])).toEqual([
+      expect(pipe([1, 2], zip(["a", "b", "c"]))).toStrictEqual([
         [1, "a"],
         [2, "b"],
       ]);
     });
+
+    test("evaluates lazily", () => {
+      const mockFn = vi.fn(identity());
+      pipe([1, 2, 3], map(mockFn), zip([4, 5, 6]), first());
+      expect(mockFn).toBeCalledTimes(1);
+    });
   });
 });
 
diff --git a/src/zipWith.test.ts b/src/zipWith.test.ts
index d849d6f..a9a7f13 100644
--- a/src/zipWith.test.ts
+++ b/src/zipWith.test.ts
@@ -1,65 +1,109 @@
+import { pipe } from "./pipe";
 import { zipWith } from "./zipWith";
 
-const pred = (a: string, b: string): string => a + b;
+describe("runtime", () => {
+  describe("data first", () => {
+    test("should zip with predicate", () => {
+      expect(
+        zipWith(["1", "2", "3"], ["a", "b", "c"], (a, b) => `${a}${b}`),
+      ).toStrictEqual(["1a", "2b", "3c"]);
+    });
 
-const first = ["1", "2", "3"];
-const second = ["a", "b", "c"];
-const shorterFirst = ["1", "2"];
-const shorterSecond = ["a", "b"];
+    test("should truncate to shorter second", () => {
+      expect(
+        zipWith(["1", "2", "3"], ["a", "b"], (a, b) => `${a}${b}`),
+      ).toStrictEqual(["1a", "2b"]);
+    });
 
-describe("data first", () => {
-  test("should zip with predicate", () => {
-    expect(zipWith(first, second, pred)).toEqual(["1a", "2b", "3c"]);
+    test("should truncate to shorter first", () => {
+      expect(
+        zipWith(["1", "2"], ["a", "b", "c"], (a, b) => `${a}${b}`),
+      ).toStrictEqual(["1a", "2b"]);
+    });
   });
-  test("should truncate to shorter second", () => {
-    expect(zipWith(first, shorterSecond, pred)).toEqual(["1a", "2b"]);
-  });
-  test("should truncate to shorter first", () => {
-    expect(zipWith(shorterFirst, second, pred)).toEqual(["1a", "2b"]);
-  });
-});
 
-describe("data first typings", () => {
-  test("infers typing from predicate", () => {
-    const actual = zipWith(first, second, pred);
-    expectTypeOf(actual).toEqualTypeOf<Array<string>>();
-  });
-});
+  describe("data second", () => {
+    test("should zip with predicate", () => {
+      expect(
+        zipWith((a: string, b: string) => `${a}${b}`)(
+          ["1", "2", "3"],
+          ["a", "b", "c"],
+        ),
+      ).toStrictEqual(["1a", "2b", "3c"]);
+    });
 
-describe("data second", () => {
-  test("should zip with predicate", () => {
-    expect(zipWith(pred)(first, second)).toEqual(["1a", "2b", "3c"]);
-  });
-  test("should truncate to shorter second", () => {
-    expect(zipWith(pred)(first, shorterSecond)).toEqual(["1a", "2b"]);
+    test("should truncate to shorter second", () => {
+      expect(
+        zipWith((a: string, b: string) => `${a}${b}`)(
+          ["1", "2", "3"],
+          ["a", "b"],
+        ),
+      ).toStrictEqual(["1a", "2b"]);
+    });
+
+    test("should truncate to shorter first", () => {
+      expect(
+        zipWith((a: string, b: string) => `${a}${b}`)(
+          ["1", "2"],
+          ["a", "b", "c"],
+        ),
+      ).toStrictEqual(["1a", "2b"]);
+    });
   });
-  test("should truncate to shorter first", () => {
-    expect(zipWith(pred)(shorterFirst, second)).toEqual(["1a", "2b"]);
+
+  describe("data second with initial arg", () => {
+    test("should zip with predicate", () => {
+      expect(
+        pipe(
+          ["1", "2", "3"],
+          zipWith(["a", "b", "c"], (a, b) => `${a}${b}`),
+        ),
+      ).toStrictEqual(["1a", "2b", "3c"]);
+    });
+
+    test("should truncate to shorter second", () => {
+      expect(
+        pipe(
+          ["1", "2", "3"],
+          zipWith(["a", "b"], (a, b) => `${a}${b}`),
+        ),
+      ).toStrictEqual(["1a", "2b"]);
+    });
+
+    test("should truncate to shorter first", () => {
+      expect(
+        pipe(
+          ["1", "2"],
+          zipWith(["a", "b", "c"], (a, b) => `${a}${b}`),
+        ),
+      ).toStrictEqual(["1a", "2b"]);
+    });
   });
 });
 
-describe("data second typings", () => {
-  test("infers typing from predicate", () => {
-    const actual = zipWith(pred)(first, second);
+describe("typing", () => {
+  test("data first typings", () => {
+    const actual = zipWith(
+      ["1", "2", "3"],
+      ["a", "b", "c"],
+      (a, b) => `${a}${b}`,
+    );
     expectTypeOf(actual).toEqualTypeOf<Array<string>>();
   });
-});
 
-describe("data second with initial arg", () => {
-  test("should zip with predicate", () => {
-    expect(zipWith(pred, second)(first)).toEqual(["1a", "2b", "3c"]);
-  });
-  test("should truncate to shorter second", () => {
-    expect(zipWith(pred, shorterSecond)(first)).toEqual(["1a", "2b"]);
-  });
-  test("should truncate to shorter first", () => {
-    expect(zipWith(pred, second)(shorterFirst)).toEqual(["1a", "2b"]);
+  test("data second typings", () => {
+    const actual = zipWith((a: string, b: string) => `${a}${b}`)(
+      ["1", "2", "3"],
+      ["a", "b", "c"],
+    );
+    expectTypeOf(actual).toEqualTypeOf<Array<string>>();
   });
-});
 
-describe("data second with initial arg typings", () => {
-  test("infers typing from predicate", () => {
-    const actual = zipWith(pred, second)(first);
+  test("data second with initial arg typings", () => {
+    const actual = pipe(
+      ["1", "2", "3"],
+      zipWith(["a", "b", "c"], (a, b) => `${a}${b}`),
+    );
     expectTypeOf(actual).toEqualTypeOf<Array<string>>();
   });
 });