Skip to content

Commit

Permalink
feat(v2): reduceLazy -> purryFromLazy, +takeLast, lazy zip, built-in …
Browse files Browse the repository at this point in the history
…flat, and more... (#672)

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.
  • Loading branch information
eranhirsch committed May 10, 2024
1 parent 2446135 commit ac35885
Show file tree
Hide file tree
Showing 48 changed files with 717 additions and 477 deletions.
15 changes: 5 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion docs/src/content/v1-migration/_mapToObj.md

This file was deleted.

44 changes: 44 additions & 0 deletions docs/src/content/v1-migration/mapToObj.md
Original file line number Diff line number Diff line change
@@ -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 }
```
2 changes: 1 addition & 1 deletion docs/src/content/v1-migration/mapWithFeedback.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions docs/src/content/v1-migration/zipWith.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions docs/src/lib/v1/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
4 changes: 4 additions & 0 deletions src/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
6 changes: 3 additions & 3 deletions src/addProp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand All @@ -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 };

Expand All @@ -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,
Expand Down
14 changes: 4 additions & 10 deletions src/difference.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
};
}
16 changes: 4 additions & 12 deletions src/differenceWith.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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 =
Expand All @@ -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;
5 changes: 5 additions & 0 deletions src/divide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
71 changes: 45 additions & 26 deletions src/drop.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
13 changes: 8 additions & 5 deletions src/drop.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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 };
};
Expand Down

0 comments on commit ac35885

Please sign in to comment.