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(functions): adding a functions module with pipe() #6143

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

guy-borderless
Copy link
Contributor

@guy-borderless guy-borderless commented Oct 24, 2024

Hi,
I'd like to suggest adding an std module for utilities related to functions.

related: #4386

There are common manipulations to functions implemented in numerous npm packages and toolbelts, and using them where appropriate greatly improves code quality. It would be handy to have these in deno_std.
I want to suggest for starters the following two:

  1. pipe
  2. set_arguments (currying / dependency injection)

These two scenarios have some typescript knowledge behind them and it makes sense to reach for a proper utility.

Both of these have many different implementations in terms of code and typings. In my opinion, the implementation most in line with the pragmatic spirit of the std should avoid functional programming jargon for simplicity and accessibility to beginners.

For discussion in this PR I have included two implementations (without tests) that I think would suit the deno_std

pipe has good typescript performance and small implementation. I'm also using it in several projects.

set_arguments has a very non-academic intuitive signature that I think suites nicely with the deno_std. This signature does not expect a data-last coding convention, so it doesn't leak to the rest of the code. I have also provided a more classic functional programming implementation of this called curry to contrast.

I can significantly simplify the typing definition of set_arguments if you'd like me to go ahead with preparing this PR.

Since functions are an important primitive in JS, I'm sure more utilities will be added.

@guy-borderless guy-borderless requested a review from kt3k as a code owner October 24, 2024 07:38
@kt3k
Copy link
Member

kt3k commented Oct 24, 2024

Are pipe and set_arguments common in JS ecosystem? I rarely see them in various code bases.

I personally think inline arrow functions express the operation like set_arguments better.

const myNewFunc = set_arguments(myFunc, 1, undefined, 3);
// vs
const myNewFunc = (x: number) => myFunc(1, x, 3);

@guy-borderless
Copy link
Contributor Author

Are pipe and set_arguments common in JS ecosystem? I rarely see them in various code bases.

I personally think inline arrow functions express the operation like set_arguments better.

const myNewFunc = set_arguments(myFunc, 1, undefined, 3);
// vs
const myNewFunc = (x: number) => myFunc(1, x, 3);

Both curry and pipe are very common and implemented by all lodash style toolbelts (including lodash of course), as well as many specific packages. Your example works because you have the number primitive. In cases of a business object, which you may or may not control, it's much nicer to use something like curry or set_arguments.

pipe is a common pattern, for example in ETL code when you have a lot of linear transformations, it makes the code much more readable, as it prevents spaghetti.

@kt3k
Copy link
Member

kt3k commented Oct 24, 2024

Your example works because you have the number primitive.

I don't understand this. Inline arrow function example should work with any parameters available in that scope. I think that has the same capability as what set_arguments (or curry) can do.

BTW do you suggest curry or set_arguments? or both?

pipe is a common pattern, for example in ETL code when you have a lot of linear transformations, it makes the code much more readable, as it prevents spaghetti.

Can you elaborate on more specific examples where pipe is better for readability? The given example looks too trivial to replace with simple function composition.

const func = pipe(Math.abs, Math.sqrt, Math.floor);
//
const func = (num: number) => Math.floor(Math.sqrt(Math.abs(num)))

@kt3k kt3k added the feedback welcome We want community's feedback on this issue or PR label Oct 24, 2024
@guy-borderless
Copy link
Contributor Author

guy-borderless commented Oct 25, 2024

Your example works because you have the number primitive.

I don't understand this. Inline arrow function example should work with any parameters available in that scope. I think that has the same capability as what set_arguments (or curry) can do.

BTW do you suggest curry or set_arguments? or both?

pipe is a common pattern, for example in ETL code when you have a lot of linear transformations, it makes the code much more readable, as it prevents spaghetti.

Can you elaborate on more specific examples where pipe is better for readability? The given example looks too trivial to replace with simple function composition.

const func = pipe(Math.abs, Math.sqrt, Math.floor);
//
const func = (num: number) => Math.floor(Math.sqrt(Math.abs(num)))

I see set_arguments as just a less functional version of curry, so if we want to be more strict it's either, depending on the policy.

Yes, of course, you can create an arrow function to inject parameters:

const fnWithFirstDeps = (dataArg: Parameters<typeof fn>[2]) => fn(dependency1, dependency2, dataArg)

I think it's a common and rather generic need, so having a utility that obviates the need to create a local function for it and improves readability is handy, while also encouraging a good coding practice (dependency injection). Sometimes different parts of the code inject different dependencies, having a utility encourages doing the injection locally and not creating another function or re-using the above function which entangles concerns.

const fnWithFirstDeps = setArguments(fn, dependency1, dependency2,)

pipe is useful in data processing. You don't want to compose many functions to create a pipe as this creates long bracket scopes and isn't readable. Here is a less trivial example:

const extracted = {
  bedrooms:  pipe(
      selectOne.attributes,
      parseMatchingText("bedrooms"),
      removeLabel,
      parseNumber,
    ),
  bathrooms:  pipe(
      selectOne.attributes,
      h.parseMatchingText("bathrooms"),
      removeLabel,
      parseNumber,
    ),
}

@kt3k
Copy link
Member

kt3k commented Oct 28, 2024

I'm also not a fan of these typings. curry and setArguments seem having limits in number of arguments, while inline arrow function composition doesn't have such limitation. The type of pipe function looks extremely hard to understand. I guess the type error message will be also harder to understand with pipe than plain function composition.

@kt3k
Copy link
Member

kt3k commented Oct 28, 2024

(BTW while I'm personally not in favor of this, we are open for adding this package if there's enough community support to this idea.)

@guy-borderless
Copy link
Contributor Author

I'm also not a fan of these typings. curry and setArguments seem having limits in number of arguments, while inline arrow function composition doesn't have such limitation. The type of pipe function looks extremely hard to understand. I guess the type error message will be also harder to understand with pipe than simpler function composition.

Typing for a number of arguments is very common and gives a good experience. As far as I can see, it's a theoretical, not a practical issue.

@iuioiua
Copy link
Contributor

iuioiua commented Nov 14, 2024

This PR and #4386, which has been open since February, have had near-zero community support. Perhaps, it'd be best to have this done as a 3rd party package and see if it evolves enough to provide a stronger argument for its addition sometime in the future.

@Kacaii
Copy link

Kacaii commented Nov 21, 2024

Honestly pipe would help a ton. Looking forward to this one!

@kt3k
Copy link
Member

kt3k commented Nov 21, 2024

@guy-borderless Can you limit this PR to pipe function? Also can you write test cases for it?

@guy-borderless
Copy link
Contributor Author

@guy-borderless Can you limit this PR to pipe function? Also can you write test cases for it?

Of course! On it.

Copy link

codecov bot commented Dec 18, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 96.37%. Comparing base (2604b37) to head (aa42c90).
Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6143      +/-   ##
==========================================
- Coverage   96.43%   96.37%   -0.06%     
==========================================
  Files         547      552       +5     
  Lines       41812    41895      +83     
  Branches     6347     6350       +3     
==========================================
+ Hits        40321    40377      +56     
- Misses       1450     1478      +28     
+ Partials       41       40       -1     

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

@guy-borderless guy-borderless changed the title functions-module feat(functions): adding a functions module with pipe() Dec 18, 2024
@guy-borderless
Copy link
Contributor Author

Per feedback, PR was limited to pipe() and tests were added.

@BlackAsLight
Copy link
Contributor

Per feedback, PR was limited to pipe() and tests were added.

You also got to get all the tests to pass. Looks like the title isn't named right and you're missing the copyright notice in some of the files.

@guy-borderless
Copy link
Contributor Author

Per feedback, PR was limited to pipe() and tests were added.

You also got to get all the tests to pass. Looks like the title isn't named right and you're missing the copyright notice in some of the files.

The PR naming issue seems to be because the functions module is new and so a scope doesn't exist for it. Added the copyright notice, thanks for mentioning it (didn't seem to cause a test to fail)

deno.json Outdated Show resolved Hide resolved
@BlackAsLight
Copy link
Contributor

The PR naming issue seems to be because the functions module is new and so a scope doesn't exist for it.

I believe a change needs to happen in .github/workflows/title.yml to recognise the functions scope. Possibly other places in .github/ as well.

deno.json Outdated Show resolved Hide resolved
functions/mod.ts Outdated Show resolved Hide resolved
@kt3k kt3k changed the title feat(functions): adding a functions module with pipe() feat(functions): adding a functions module with pipe()_ Dec 19, 2024
@kt3k kt3k changed the title feat(functions): adding a functions module with pipe()_ feat(functions): adding a functions module with pipe() Dec 19, 2024
@guy-borderless
Copy link
Contributor Author

should I add the functions module to import_map.json here?

@BlackAsLight
Copy link
Contributor

should I add the functions module to import_map.json here?

Yes

functions/mod.ts Outdated Show resolved Hide resolved
@kt3k
Copy link
Member

kt3k commented Jan 6, 2025

cc @andrewthauer Do you find this pipe function useful for your use case?

@guy-borderless
Copy link
Contributor Author

cc @andrewthauer Do you find this pipe function useful for your use case?

I would note that the pipe() implementation and signature here hasn't changed since @andrewthauer's last comment and that there aren't many options for a pipe signature, and all are pretty equivalent IMO. This implementation has better typescript behavior than the typical pipe(), all thanks to @ecyrbe, but that's about it.

* @param input The functions to be composed
* @returns A function composed of the input functions, from left to right
*/
export function pipe(): <T>(arg: T) => T;
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this first overload? What is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is my habit that if an edge case has a natural interpretation I include it by default, even without a concrete use-case. This isn't a strong opinion, happy to remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would note that a scenario for this will, of course, involve .apply(). Again, I don't mind.

@andrewthauer
Copy link
Contributor

cc @andrewthauer Do you find this pipe function useful for your use case?

Sorry just saw this. I'll pull the PR down and play around to see how well it works in regards to addressing #4386

@andrewthauer
Copy link
Contributor

andrewthauer commented Jan 20, 2025

First, I will say, that the behaviour of pipe functions varies across libraries for what they call pipe. So first off I'd like to loosely classify this version as a data-last curried pipe function that works left to right.

Some libraries auto curry based on the data or function args or do not curry at all. Others distinguish the difference with something like a flow function (e.g. fs-ts's flow) vs pipe.

Ok, so here are my initial thoughts on the pipe function in it's current state:

Pipeline typing works pretty well through pipeline

const myPipe = pipe(
  (num: number) => String(num),
  (str) => Number(str),
);

myPipe(3)
^^^ accepts number returns string!

Pipeline function arguments are not inferred

This is where typing could be improved ...

const myPipe = pipe(
  (num: number) => String(num),
  (str) => Number(str),
  ^^^ Ideally this would be inferred as a string
);

Does not support for multiple arguments in first function

Not sure this is a show stopper, but it would be nice to support multiple arguments in the first function. The fs-ts flow function supports this which is nice.

const myPipe = pipe(
  (a: number, b: number) => a + b,
  (str) => Number(str),
);

myPipe2(3, 2);
        ^^^^ expected 1 args but provided 2                

In general this is a good start with initial testing, but could have improved typing imo (fs-ts does a good job here), If I need a quick pipe and didn't already have another functional library pulled in I might make use of it. That said, without a builtin curry/purry function it would likely be a bit verbose to define a pipeline of functions that have multiple arguments (non unary arity). Although this is perhaps not a fault of the pipe function itself. However, the lack of a curry function makes it less useful from a functional perspective imo.

@guy-borderless
Copy link
Contributor Author

guy-borderless commented Jan 20, 2025 via email

@guy-borderless
Copy link
Contributor Author

Just mentioning that function arguments should be inferred in this implementation. Checking when near a computer

On Mon, Jan 20, 2025, 07:56 Andrew Thauer @.> wrote: First, I will say, that the behaviour of pipe functions varies across libraries for what they call pipe. So first off I'd like to loosely classify this version as a data-last curried pipe function that works left to right. Some libraries auto curry based on the data or function args or do not curry at all. Others distinguish the difference with something like a flow function (e.g. fs-ts's flow https://gcanti.github.io/fp-ts/modules/function.ts.html#flow) vs pipe https://gcanti.github.io/fp-ts/modules/function.ts.html#pipe. Ok, so here are my initial thoughts on the pipe function in it's current state: Pipeline typing works pretty well through pipeline const myPipe = pipe( (num: number) => String(num), (str) => Number(str),); myPipe(3)^^^ accepts number returns string! Pipeline function arguments are not inferred This is where typing could be improved ... const myPipe = pipe( (num: number) => String(num), (str) => Number(str), ^^^ Ideally this would be inferred as a string); Does not support for multiple arguments in first function Not sure this is a show stopper, but it would be nice to support multiple arguments in the first function. The fs-ts flow https://gcanti.github.io/fp-ts/modules/function.ts.html#flow function supports this which is nice. const myPipe = pipe( (a: number, b: number) => a + b, (str) => Number(str),); myPipe2(3, 2); ^^^^ expected 1 args but provided 2 In general this is a good start with initial testing, but could have improved typing imo (fs-ts does a good job here), If I need a quick pipe and didn't already have another functional library pulled in I might make use of it. That said, without a builtin curry/purry function it would likely be a bit verbose to define a pipeline of functions that have multiple arguments (non unary arity). Although this is perhaps not a fault of the pipe function itself. However, the lack of a curry function makes it less unseful from a functional perspective imo. — Reply to this email directly, view it on GitHub <#6143 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AX35W655K5SWMCER4ZVKDTD2LRCVLAVCNFSM6AAAAABQQN6GY6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMMBRGEYTSNZQG4 . You are receiving this because you were mentioned.Message ID: @.>

First, I will say, that the behaviour of pipe functions varies across libraries for what they call pipe. So first off I'd like to loosely classify this version as a data-last curried pipe function that works left to right.

Some libraries auto curry based on the data or function args or do not curry at all. Others distinguish the difference with something like a flow function (e.g. fs-ts's flow) vs pipe.

Ok, so here are my initial thoughts on the pipe function in it's current state:

Pipeline typing works pretty well through pipeline

const myPipe = pipe(
  (num: number) => String(num),
  (str) => Number(str),
);

myPipe(3)
^^^ accepts number returns string!

Pipeline function arguments are not inferred

This is where typing could be improved ...

const myPipe = pipe(
  (num: number) => String(num),
  (str) => Number(str),
  ^^^ Ideally this would be inferred as a string
);

Does not support for multiple arguments in first function

Not sure this is a show stopper, but it would be nice to support multiple arguments in the first function. The fs-ts flow function supports this which is nice.

const myPipe = pipe(
  (a: number, b: number) => a + b,
  (str) => Number(str),
);

myPipe2(3, 2);
        ^^^^ expected 1 args but provided 2                

In general this is a good start with initial testing, but could have improved typing imo (fs-ts does a good job here), If I need a quick pipe and didn't already have another functional library pulled in I might make use of it. That said, without a builtin curry/purry function it would likely be a bit verbose to define a pipeline of functions that have multiple arguments (non unary arity). Although this is perhaps not a fault of the pipe function itself. However, the lack of a curry function makes it less useful from a functional perspective imo.
andrewthauer

Andrew, thanks for your feedback. Initially, I considered requiring all pipe functions to be unary, even though it’s unnecessary for the first one, to simplify reordering functions within the pipe if the need arise. upon reflection and given deno-std’s unopinionated approach, this restriction does feel unnecessary, so I now removed it.

I’ve added argument typings for up to 7 pipe functions and can extend it further if needed.

While I find value in using this implementation of pipe without currying, I agree that including a curry function would be very useful. It would naturally complement pipe and enhance flexibility in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feedback welcome We want community's feedback on this issue or PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants