Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(every): Add every #631

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open

feat(every): Add every #631

wants to merge 14 commits into from

Conversation

Brodzko
Copy link
Contributor

@Brodzko Brodzko commented Apr 10, 2024

Following up on conversation in #624, I started with implementing every.


Make sure that you:

  • Typedoc added for new methods and updated for changed
  • Tests added for new methods and updated for changed
  • New methods added to src/index.ts
  • New methods added to mapping.md

We use semantic PR titles to automate the release process!

https://conventionalcommits.org

PRs should be titled following using the format: < TYPE >(< scope >)?: description

Available Types:

  • feat: new functions, and changes to a function's type that would impact users.
  • fix: changes to the runtime behavior of an existing function, or refinements to it's type that shouldn't impact most users.
  • perf: changes to function implementations that improve a functions runtime performance.
  • refactor: changes to function implementations that are neither fix nor perf
  • test: tests-only changes (transparent to users of the function).
  • docs: changes to the documentation of a function or the documentation site.
  • build, ci, style, chore, and revert: are only relevant for the internals of the library.

For scope put the name of the function you are working on (either new or
existing).

Copy link

codesandbox-ci bot commented Apr 10, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 64ca4ef:

Sandbox Source
remeda-example-vanilla Configuration

Copy link

codecov bot commented Apr 10, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.62%. Comparing base (96a4e82) to head (64ca4ef).
Report is 90 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #631      +/-   ##
==========================================
+ Coverage   98.41%   98.62%   +0.20%     
==========================================
  Files         127      155      +28     
  Lines        7954    11186    +3232     
  Branches      724      941     +217     
==========================================
+ Hits         7828    11032    +3204     
- Misses        123      151      +28     
  Partials        3        3              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Brodzko Brodzko marked this pull request as draft April 10, 2024 21:27
@Brodzko Brodzko mentioned this pull request Apr 14, 2024
@eranhirsch eranhirsch changed the title Draft: feat(every): Add every feat(every): Add every Apr 14, 2024
@Brodzko Brodzko marked this pull request as ready for review April 14, 2024 12:57
mapping.md Outdated
@@ -109,6 +109,7 @@ documentation when migrating._
| [`unique`](https://remedajs.com/docs/#unique) | [`uniq`](https://lodash.com/docs/4.17.15#uniq) | [`uniq`](https://ramdajs.com/docs/#uniq) |
| [`uniqueBy`](https://remedajs.com/docs/#uniqueBy) | [`uniqBy`](https://lodash.com/docs/4.17.15#uniqBy) | [`uniqBy`](https://ramdajs.com/docs/#uniqBy) |
| [`uniqueWith`](https://remedajs.com/docs/#uniqueWith) | [`uniqWith`](https://lodash.com/docs/4.17.15#uniqWith) | [`uniqWith`](https://ramdajs.com/docs/#uniqWith) |
| [`every`](https://remedajs.com/docs/#every) | [`every`](https://lodash.com/docs/4.17.15#every) | |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's sort this

src/every.ts Outdated
Comment on lines 38 to 41
* NOTE: If you want to use this inside `conditional` predicates and preserve proper type inference,
* make sure to define the final type guard outside of the pipe. This is due to Typescript's limitation
* in generic type inference. See examples below for details.
*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what do you feel about dropping this? i think this is a problem with conditional and not every, and so notes like this should go into usage of conditional (because they also apply to other predicates)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, i would say things like this should go in one of the conditional tests as like, a known issue

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(rather than the jsdoc, which appears every time we hover over the function)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed 👍

expect(data).toBe(true);
});

test("narrows types when used with filter", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i wouldnt really think of this as a unit test... it tests filter more than it does every here, since we do already have a test of every narrowing properly?

src/every.ts Outdated
* R.every(['a', 0, 'b'], R.isString) // => false
* @dataFirst
* @indexed
* @pipeable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: i think we only use @pipeable for lazy functions, so we should remove this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(or at least, we should)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first pr after releasing v2 would be to rename pipeable as lazy

@cjquines
Copy link
Collaborator

oops sorry for the late review, was busy with my Day Job

@Brodzko
Copy link
Contributor Author

Brodzko commented Apr 19, 2024

@cjquines no worries, same here. Pushed updates to your previous comments :)

Copy link
Collaborator

@cjquines cjquines left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, will merge after todo

mapping.md Outdated
Comment on lines 30 to 31
| [`entries`](https://remedajs.com/docs/#entries) |
| [`toPairs`](https://lodash.com/docs/4.17.15#toPairs) | [`toPairs`](https://ramdajs.com/docs/#toPairs) |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: formatting here is now incorrect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I see what's wrong here exactly?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be a single line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah 🤦 ngl that took me a while to finally notice. Fixed, thanks! 🦅 👁️

@cjquines cjquines self-requested a review April 22, 2024 14:52
Copy link
Collaborator

@cjquines cjquines left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scratch that i actually have some more questions

});
});

describe("data-last", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: rewrite these tests to use pipe

i think we mostly care about data-last usages in pipes, so i'd push for these tests to be in pipes

});
});

describe("in pipe", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what does this test?

there's no inference being tested here

src/_types.ts Outdated
Comment on lines 15 to 27
export type TypePred<T, S extends T> = (input: T) => input is S;

export type TypePredIndexed<T, S extends T> = (
input: T,
index: number,
array: ReadonlyArray<T>,
) => input is S;

export type TypePredIndexedOptional<T, S extends T> = (
input: T,
index?: number,
array?: ReadonlyArray<T>,
) => input is S;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't define these here, all of these types are going away in v2. They aren't useful because the hide a very basic core concept of typing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where would be a better place to put them then, while in v1?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put the types inline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put them inline in the file, the reasoning behind putting them in _types was that I immediately needed the same thing for some in the next PR, but let's take it step by step

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eranhirsch lgty?

src/every.ts Outdated
Comment on lines 76 to 94
export namespace every {
export function indexed<T, S extends T>(
data: ReadonlyArray<T>,
predicate: TypePredIndexed<T, S>,
): data is Array<S>;
export function indexed<T>(
data: ReadonlyArray<T>,
predicate: PredIndexed<T, boolean>,
): boolean;
export function indexed<T, S extends T>(
predicate: TypePredIndexed<T, S>,
): (data: ReadonlyArray<T>) => data is Array<S>;
export function indexed<T>(
predicate: PredIndexed<T, boolean>,
): (data: ReadonlyArray<T>) => boolean;
export function indexed(): unknown {
return purry(_every(true), arguments);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the indexed version is not needed, make the regular version support the indexed version.

const data = every(isNumber)(input);
expect(data).toBe(true);
if (every(isNumber)(input)) {
expectTypeOf(input).toEqualTypeOf<Array<number>>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't mix type testing with runtime testing; they run at different times in the test cycle and thus bind two unrelated concerns.

That's why I prefer having a "runtime" block and a "typing" block.

src/every.ts Outdated
Comment on lines 44 to 53
* @example
* // using `R.every` as a type predicate within `conditional`
* const allNumbers = R.every(R.isNumber);
* const result = R.pipe(
* R.conditional(
* [allNumbers, val => {...}]
* // ^? number[]
* ...
* )
* )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we support multiple example tags in our docs. This seems like a really big example using a utility that people might not know or understand; it makes the example more complex than it should be.

Our examples aren't usually full-blown usage examples; they just show a basic, almost trivial, use case so that people understand what the params look like and how they relate to the output.

src/every.ts Outdated
return purry(_every(false), arguments);
}

const _every =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are dropping these underscore marker for private implementations. Use a full name for the function, everyImplementation

@Brodzko
Copy link
Contributor Author

Brodzko commented Apr 27, 2024

Sorry, got caught up at work. I'll try to get to the other PRs during the weekend too.

Tried addressing most of you guys' comments and posted some questions to others :)

src/every.ts Outdated
data: ReadonlyArray<T>,
predicate: PredIndexed<T, boolean>,
): boolean =>
data.every((element, index, array) => predicate(element, index, array));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out similar functions in V2, you don't need to create the arrow function here, it's redundant for our use-case

Suggested change
data.every((element, index, array) => predicate(element, index, array));
data.every(predicate);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did this, nut had to disable no-array-callback-reference for it to be legal :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that rule is disabled altogether in v2

src/every.ts Outdated
Comment on lines 4 to 8
type TypePredIndexed<T, S extends T> = (
input: T,
index: number,
array: ReadonlyArray<T>,
) => input is S;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline this, in v2 all functions have the function operand stated explicitly, these make it harder to read the function signature to understand what it accepts.

src/every.ts Outdated
@@ -0,0 +1,63 @@
import type { PredIndexed } from "./_types";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, these are removed in v2, don't use them

@Brodzko Brodzko requested a review from eranhirsch April 30, 2024 15:37
Copy link
Collaborator

@eranhirsch eranhirsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the comments so late in the development. I've been working mainly on V2 so didn't have time to check other stuff. Because the release of v2 is happening very soon and it's likely we'll merge these changes only after releasing v2 it needs to match the style for v2.

]),
);

expect(resultBroken).toBe(true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a runtime check in a typing test. Do we need this? it doesn't test anything that is related to the bug

]),
);

expect(resultCorrect).toBe(true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly

Comment on lines +7 to +15
test("returns correct values on simple arrays", () => {
const trueInput = [1, 2, 3];
const trueData = every(trueInput, (val) => val < 5);
expect(trueData).toBe(true);

const falseInput = ["foo", "bar", "buzz"];
const falseData = every(falseInput, (val) => val.length > 3);
expect(falseData).toBe(false);
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep tests simple and short; it's better to have more tests than to have tests do more; that way, when a test fails, the actual failure is reported. And we can debug the specific case instead of having to comment out non-failing parts.

Suggested change
test("returns correct values on simple arrays", () => {
const trueInput = [1, 2, 3];
const trueData = every(trueInput, (val) => val < 5);
expect(trueData).toBe(true);
const falseInput = ["foo", "bar", "buzz"];
const falseData = every(falseInput, (val) => val.length > 3);
expect(falseData).toBe(false);
});
test("all values meet the criteria", () => {
expect(every([1, 2, 3], (val) => val < 5)).toBe(true);
});
test("a value doesn't meet the criteria", () => {
expect(every(["foo", "bar", "buzz"], (val) => val.length > 3)).toBe(false);
});

expect(falseData).toBe(false);
});

// empty set check
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name describes it, we don't need documentation inside tests

Suggested change
// empty set check

Comment on lines +25 to +27
const input = [1, 2, 3];
const data = every(input, (_, index) => index < 5);
expect(data).toBe(true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼 nice, this is an advantage of moving indexed predicates to the main signature, we test them like regular logic and not disconnected from other concerns!

Comment on lines +83 to +91
const dataPipe = pipe(
input,
every((val, index, arr) => {
expectTypeOf(val).toEqualTypeOf<number>();
expectTypeOf(index).toEqualTypeOf<number>();
expectTypeOf(arr).toEqualTypeOf<ReadonlyArray<number>>();
return index < 5;
}),
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's actually a lot of cases where the piped dataLast version breaking without the dataFirst version breaking, let's split these tests

Comment on lines +78 to +79
expectTypeOf(index).toEqualTypeOf<number>();
expectTypeOf(arr).toEqualTypeOf<ReadonlyArray<number>>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to test these, it's part of the function signature, and they aren't inferred or have any "magic" that might break them.

Suggested change
expectTypeOf(index).toEqualTypeOf<number>();
expectTypeOf(arr).toEqualTypeOf<ReadonlyArray<number>>();

return true;
}),
);
expect(data).toBe(true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runtime concern in a type test

Comment on lines +18 to +21
export function every<T, S extends T>(
data: ReadonlyArray<T>,
predicate: (input: T, index: number, array: ReadonlyArray<T>) => input is S,
): data is Array<S>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typing does not respect the shape of the input in the output. We can take the tuple as a generic parameter and use it to return a result that is the same shape.

Suggested change
export function every<T, S extends T>(
data: ReadonlyArray<T>,
predicate: (input: T, index: number, array: ReadonlyArray<T>) => input is S,
): data is Array<S>;
export function every<T extends IterableContainer, S extends T[number]>(
data: T,
predicate: (input: T[number], index: number, array: T) => input is S,
): data is { [K in keyof T]: S };

This would allow someone to test something like [string | number, ...Array<string | number>] and get back [string, ...Array<string>]

The non-narrowing overload doesn't need this treatment because it returns a boolean

Comment on lines +41 to +43
export function every<T, S extends T>(
predicate: (input: T, index: number, array: ReadonlyArray<T>) => input is S,
): (data: ReadonlyArray<T>) => data is Array<S>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Suggested change
export function every<T, S extends T>(
predicate: (input: T, index: number, array: ReadonlyArray<T>) => input is S,
): (data: ReadonlyArray<T>) => data is Array<S>;
export function every<T extends IterableContainer, S extends T[number]>(
predicate: (input: T[number], index: number, array: T) => input is S,
): (data: T) => data is { [K in keyof T]: S };

@Brodzko
Copy link
Contributor Author

Brodzko commented May 1, 2024

Sorry for the comments so late in the development. I've been working mainly on V2 so didn't have time to check other stuff. Because the release of v2 is happening very soon and it's likely we'll merge these changes only after releasing v2 it needs to match the style for v2.

No worries. How soon are we talking? If preferred, I can wait for v2 to release and then re-open this with all the things we found during this review resolved

@eranhirsch
Copy link
Collaborator

It should happen in the next 2 weeks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants