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

Suggestion: instead use as sugar for a new Array.prototype.pipe #177

Closed
dustbort opened this issue Feb 7, 2021 · 9 comments
Closed

Suggestion: instead use as sugar for a new Array.prototype.pipe #177

dustbort opened this issue Feb 7, 2021 · 9 comments

Comments

@dustbort
Copy link

dustbort commented Feb 7, 2021

I have read a lot of discussion about pipeline operator and variants like optional pipeline ?>. I also know this can lead down a rabbit hole with 2- and 3-arity pipeline operators as in F#: |>, ||>, |||>, <|, <||, <|||. I have a suggestion to avoid these and allow pipeline to cover many more use cases.

What if we define a new function on Array.prototype called pipe that works as follows? pipe will operate on an array and spread the elements of the array as the arguments to a supplied function. If the return value of the applied function is nullish (null or undefined) then pipe returns that value as-is; otherwise, pipe wraps the result in an array. For example:

const add = (a, b) => a + b;

[1, 2].pipe(add) // returns [3]

This has the nice property that it works in conjunction with the optional chaining operator
?., so that we can short circuit multiple chained pipes when we encounter a nullish return value:

[x].pipe(foo)?.pipe(bar)?.pipe(baz)

What if the applied function returns an n-tuple to which we want to apply a subsequent n-arity transformation with pipe? We've wrapped it in an extra array in that case. No problem, let's define another function on Array.prototype called flatPipe. It works like pipe, except it does not wrap the result in an array.

Now we can chain pipes and flatPipes to our heart's desire, with transformations of any arity using only these two pipeline functions. And we can short-circuit with the existing optional chaining ?..

Let's revisit the proposed pipeline operator in light of pipe and flatPipe. Let's define:

  • a |> b := a.pipe(b)
  • a ?|> b := a?.pipe(b)
  • a |>> b := a.flatPipe(b)
  • a ?|>> b := a?.flatPipe(b)

This is very nice. The benefit of having to wrap a scalar value in an array in the 1-arity case is that we don't define the pipeline operator based on the exact arity of the tuple, only whether it is returned as a scalar or a tuple. This means we can have a 5-arity pipeline without defining |||||>, etc. Rather, the operator is chosen based on how we need to adapt the return value of the function to work in a pipeline, either scalar or tuple.

@dustbort
Copy link
Author

dustbort commented Feb 8, 2021

@samhh Could you share what you find confusing? I would like to clarify.

@samhh
Copy link

samhh commented Feb 8, 2021

Hey @dustbort. Sorry for that reaction, it was a lazy one whilst I ticked through my list of GitHub notifications this morning.

The most fundamental criticism I can offer is that as an fp-ts user who uses pipe everyday, were this proposal approved, I wouldn't use it. Users of a pipeline operator are relatively likely to utilise currying, making the matter of variadic arity irrelevant, at which point this is more verbose than what I'd already write today, namely:

pipe(x, foo(f), bar, baz(g))

In my view, this proposal is more noisy and as a result less readable, and, though this could just be due to unfamiliarity, the underlying array behaviour is not intuitive. It's as far as I know different to pipelines in every other language.

As for nullability, I believe the first step should be to encourage userland helpers. This is in essence what I and many others already do with types like Option/Maybe. A simpler alternative could be trivially defined for more typical users that'd look something like:

x |> foo |> nullMap(bar) |> nullMap(baz)
// or
x |> foo |> nullMap(y => y |> bar |> baz)

I'm not convinced that choosing a path forward for pipelines should be determined by trying to shoehorn in preexisting operators, and picking a style that in my view functional programmers aren't even likely to use is questionable. But I can't speak for anyone else, so more perspectives would be helpful.

@dustbort
Copy link
Author

dustbort commented Feb 8, 2021

Users of a pipeline operator are relatively likely to utilise currying

JavaScript doesn't offer currying, and this proposal doesn't introduce it.

making the matter of variadic arity irrelevant

No. If that were the case, then F# wouldn't have operators for each arity. Plus, it is easy to simulate currying for partial application using arrow functions: [1,2].pipe((args) => (thirdArg) => funcThatTakesThreeArgs(...args, thirdArg))

pipe(x, foo(f), bar, baz(g))

pipe can be amended to take a variable number of arguments, which works as if chaining pipe for each argument. That's a nice suggestion to make it less noisy if if fits the use case. Thank you. I don't know if that will play nicely with e.g. TypeScript to be able to describe a function with variable number of generic types.

It's as far as I know different to pipelines in every other language.

True, but what about having an operator for each arity? Can't we do better? The array behavior is to make a scalar into a 1-tuple, which makes pipe act consistently regarding spreading the tuple as arguments.

As for nullability, I believe the first step should be to encourage userland helpers.

Regarding noise, your example here looks noisy. And I don't see short-circuiting.

This is in essence what I and many others already do with types like Option/Maybe.

JavaScript doesn't have Option, and this proposal doesn't introduce it. Optional chaining ?. based on nullish was recently added, and this issue builds on that.

@samhh
Copy link

samhh commented Feb 8, 2021

You can curry in JavaScript manually by just returning a series of unary functions.

The short-circuiting in my example would be nullMap wherein you'd simply check if the value is nullable and if not apply the function.

@dustbort
Copy link
Author

dustbort commented Feb 8, 2021

You can curry in JavaScript manually by just returning a series of unary functions.

Of course, as in any language. How does that address the issue?

The short-circuiting in my example would be nullMap wherein you'd simply check if the value is nullable and if not apply the function.

That's not short circuiting. It's still going to execute every step in the chain, although each step will just be checking if it is nullish, but that's the whole point of short-circuiting with optional chaining.

@samhh
Copy link

samhh commented Feb 8, 2021

I think we're now arguing about semantics. The behaviour for the end user is the same, except in my case where you define that behaviour yourself it's more flexible and, as I've said, more fundamentally useful for the developers most likely to utilise pipelines at all.

@dustbort
Copy link
Author

dustbort commented Feb 8, 2021

The semantics of optional chaining is a key part of the issue, to cover a common case.

Would you find the following operator semantics more intuitive? Now |> and ?|> operate on scalars.

  • a |> b := [a].pipe(b)[0]
  • a ?|> b := (a == null) ? a : [a].pipe(b)[0]
  • a |>> b := a.flatPipe(b)
  • a ?|>> b := a?.flatPipe(b)

The actual implementation of the operators can be optimized to eliminate superfluous use of arrays. You're free not to use ?|> and ?|>> and to write these out with |> and |>> if you wish, but other people might find them to be as useful as ?. is.

@samhh
Copy link

samhh commented Feb 8, 2021

I wouldn't personally add anything except |> at this stage for the same reasons I'm not a fan of this array-prototypal piping. We're getting quite far into subjective territory here though.

@dustbort
Copy link
Author

dustbort commented Feb 8, 2021

Thanks for your input. After rereading through the issues for this proposal, I can simply suggest |>> without backing functions. The rest is covered by other issues.

@dustbort dustbort closed this as completed Feb 8, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants