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(partialBindTo): Add partialBindTo, partialRightBindTo #466

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

Conversation

cjquines
Copy link
Collaborator

closes #458

adds partial, partialRight functions


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 Jan 20, 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 5ecb0c3:

Sandbox Source
remeda-example-vanilla Configuration

Copy link

codecov bot commented Feb 11, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.54%. Comparing base (96a4e82) to head (5ecb0c3).
Report is 48 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #466      +/-   ##
==========================================
+ Coverage   98.41%   98.54%   +0.12%     
==========================================
  Files         127      137      +10     
  Lines        7954     9251    +1297     
  Branches      724      779      +55     
==========================================
+ Hits         7828     9116    +1288     
- Misses        123      132       +9     
  Partials        3        3              

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

@eranhirsch
Copy link
Collaborator

I'm sorry it took me so long to review this. I'm having a hard time finding the unique value of this function over modern arrow functions. I must admit that when I said I was supportive of this, I thought we were discussing a function that handles objects and not functions.

I understand this is part of lodash, but that is due to lodash being a very old library that predates ES6, which added arrow functions to the language. Can you show a use-case where this would provide substantial benefit over using arrow functions?

cc: @viveleroi

@cjquines
Copy link
Collaborator Author

🤷 i personally dont have one

are you thinking about https://ramdajs.com/docs/#partialObject ?

@viveleroi
Copy link
Contributor

The problem with arrow functions is that we'd have to either redefine all target function arguments and types or use the rest operator and exclude the param we're overriding in the Parameters<> type. Using partial/partialRight simply reduces boilerplate when we need this.

We don't need this very often but here's an example - a grid library provides a cell formatter and we provide a modified version that uses our defaultDateFormat const.

valueFormatter: partialRight(cellDateFormatter, [defaultDateFormat])

@eranhirsch
Copy link
Collaborator

eranhirsch commented Feb 11, 2024

valueFormatter: partialRight(cellDateFormatter, [defaultDateFormat])

I would assume that valueFromatter is already typed, so it would provide the types for typescript to infer into your arrow function:

valueFormatter: (...args) => cellDateFormatter(...args, [defaultDateFormat]);

But regardless, I get your broader point about the fact that typescript not being able to infer "backwards" from the function implementation to the function signature makes certain typing situations harder. It's still not a very useful utility because it doesn't solve many other common cases (like functions that take an "options" object as its param). I'm OK with these functions, but I think they should be renamed to include some hint that they are used for functions. One name that comes to mind from the Lodash documentation of partial itself is partialBind. Would that work for everyone?

@eranhirsch
Copy link
Collaborator

@cjquines - going over the code, I'm curious to get your thoughts on the API for this function. I feel like the variadic args, although that's how it is done in lodash, is actually less useful than how it's done in Ramda where the args are an array. Making the function take an array and a function, and flipping the order of things so that the args are the "data" component, and the function is the param, e.g. partial(args, func) would allow us to implement a dataLast version, e.g. partial(func)(args) which could be used in a pipe.

This would allow things like:

const partialFoo = pipe(
  data,
  map(...),
  filter(...),
  groupBy(...),
  values,
  ...,
  partial(foo),
);
const value = partialFoo(...otherArgs);

Whereas the current implementation doesn't permit it.

This does make the "partial" name even less suitable, though, as the "data" now isn't the function at all; it's the arguments. Maybe assign, assignArguments, augmentFunction, useWith, or some other name...

@eranhirsch
Copy link
Collaborator

ChatGPT thinks that partialApply, bindArgs, preApply, and prependArgs are valid/good names too.

@cjquines
Copy link
Collaborator Author

i do like that api better (and it is something that'll be more useful in my code too). let me rewrite the function and we can bikeshed the name—don't have any opinions right now

@cjquines
Copy link
Collaborator Author

ok done—as for the name, i am liking preApply/postApply and prependArgs/appendArgs

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.

This looks great, let's go over the points I added, and if you agree with them, incorporate them throughout (i only commented on the dataFirst of partial.

And let's decide on a name for the functions. I like partiallyBindTo so that we keep the partial part for people coming from Lodash and Ramda, but make it a little more easier to grok when reading the code for someone who doesn't know the function. the To reverses the roles so that the function provided is the "param" and not the "data".

src/partial.ts Outdated Show resolved Hide resolved
src/partial.ts Outdated Show resolved Hide resolved
src/partial.ts Outdated Show resolved Hide resolved
src/partial.ts Outdated Show resolved Hide resolved
src/partial.ts Outdated
Comment on lines 31 to 35
export function partial<
A extends ReadonlyArray<unknown>,
B extends ReadonlyArray<unknown>,
C,
>(args: [...A], func: (...args: B) => C): (...rest: RemovePrefix<B, A>) => C;
Copy link
Collaborator

Choose a reason for hiding this comment

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

To get a more generic and flexible implementation we can use a function type instead of just the array of it's arguments, this provides typescript more power to infer for itself the types:

Suggested change
export function partial<
A extends ReadonlyArray<unknown>,
B extends ReadonlyArray<unknown>,
C,
>(args: [...A], func: (...args: B) => C): (...rest: RemovePrefix<B, A>) => C;
export function partial<
T extends IterableContainer,
// This is how functions are defined in ReturnType and Paramters by typescript
F extends (...args: any) => any,
>(data: T, func: F): (...rest: RemovePrefix<Parameters<F>, T>) => ReturnType<F>;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

makes sense. note that data has to be data: [...T] for typescript to infer properly though (for some reason)

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 solved by using our IterableContainer helper type (i tried it!), I think it has something to do with how typescript infers tuples generically.

Inspired by:
https://github.com/microsoft/TypeScript/blob/2a0edf7949ad8427e3ef233a4c549ecbe32953e1/src/lib/es2015.promise.d.ts#L21

@cjquines
Copy link
Collaborator Author

i settled with partialBindTo and partialRightBindTo; note that in this case partialRightBindTo is now the longest function name 🎉

@cjquines cjquines changed the title feat(partial): Add partial, partialRight feat(partialBindTo): Add partialBindTo, partialRightBindTo Feb 13, 2024
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.

There's a lot that could go wrong with typing in the unhappy flows, let's add tests to cover as much cases as possible to we can assure users that the function can handle their needs.

src/partialBindTo.ts Show resolved Hide resolved
src/partialBindTo.ts Outdated Show resolved Hide resolved
src/partialBindTo.ts Outdated Show resolved Hide resolved
src/partialBindTo.ts Outdated Show resolved Hide resolved
src/partialRightBindTo.ts Outdated Show resolved Hide resolved
src/partialBindTo.ts Outdated Show resolved Hide resolved
src/partialBindTo.test.ts Show resolved Hide resolved
});
});

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.

dataLast should also test inference via pipe, as those work a bit differently to simple dataLast invocations. Because dataLast is mainly used in pipes those are the most important cases to test.

src/partialBindTo.test.ts Show resolved Hide resolved
@cjquines
Copy link
Collaborator Author

ah, yes, the current types indeed aren't great:

const fn = (x: number, y: number, z: number) => `${x}, ${y}, and ${z}`;

const data: Array<string> = [];

// this should error:
partialBindTo(data, fn); // (...rest: unknown[]) => string
const fn = (...xs: Array<number>) => xs.join(',');

// this should not error:
partialBindTo([1, 2, 3], fn)(4, 5, 6); // error
const fn = (x: number = 1) => x;

// this should not error:
partialBindTo([2], fn)(); // error

any thoughts on this?

@eranhirsch
Copy link
Collaborator

We'll need to recursively consume the Prefix and Suffix to cover all cases. Check out my changes and the extra tests. I don't have enough time to follow the changes to the partialRight function, and I think we need to add more tests, would appreciate your help on this.

@cjquines
Copy link
Collaborator Author

cjquines commented Feb 15, 2024

this still doesn't handle a similar example to the first one i gave above: playground link

this ends up being the type inferred in cases like

pipe(
  [1],
  partialBindTo(fn),
);

@cjquines
Copy link
Collaborator Author

ok how about this: playground link

 type RemovePrefix<
   T extends IterableContainer,
   Prefix extends IterableContainer,
 > = Prefix['length'] extends 0
   ? T
   : T['length'] extends 0
-    ? RemedaTypeError<'Too many args provided to function'>
+    ? Prefix extends Array<any>
+      ? T
+      : RemedaTypeError<'Too many args provided to function'>
     : T extends [infer THead, ...infer TRest]
       ? Prefix extends [infer PrefixHead, ...infer PrefixRest]
         ? PrefixHead extends THead
           ? RemovePrefix<TRest, PrefixRest>
           : RemedaTypeError<'Argument of the wrong type provided to function'>
-        : RemedaTypeError<1>
+        : Prefix extends Array<THead>
+          ? [THead?, ...RemovePrefix<TRest, Prefix>]
+          : RemedaTypeError<1>
       : T extends [(infer THead)?, ...infer TRest]
         ? Prefix extends [infer PrefixHead, ...infer PrefixRest]
           ? PrefixHead extends THead
             ? RemovePrefix<TRest, PrefixRest>
             : RemedaTypeError<'Argument of the wrong type provided to function'>
           : Prefix extends TRest
             ? TRest
             : RemedaTypeError<2>
         : RemedaTypeError<3>;

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.

Add partial, partialRight
3 participants