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

Optional Hackpipes proposal #198

Open
webduvet opened this issue Sep 9, 2021 · 17 comments
Open

Optional Hackpipes proposal #198

webduvet opened this issue Sep 9, 2021 · 17 comments
Labels
enhancement New feature or request follow-on proposal Discussion about a future follow-on proposal

Comments

@webduvet
Copy link

webduvet commented Sep 9, 2021

The optional hackpipes proposal |?> caught my eye.

A short-circuiting optional-pipe operator |?> could also be useful, much in the way ?. is useful for optional method calls.

Instead of using |?> in any subsequent pipe in order to take care of undefined branch I would propose two constructs:

value |?> expr
value |*> expr

The rules

value |?> expr

Expresion would execute only if value us not undefined. If the input value is undefined It would skip the expression and continue to the next pipe.

value |*> expr

The second one would abort pipe if undefined value is passed thus leaving the final value of the composed structure undefined.

example:

value
  |?> one(^) // execute expression only if the input value is not undefined, otherwise skip to next pipe
  |*> two(^) // execute expression only if the input value is not undefined, otherwise abort pipe
  |> three(^) // this would execute only if the pipe is not aborted.

the above could be extended to get more granular control over pipes

value
  |?![null, false, myVar]> one(^) // execute only if passed value does not match any in the list (in addition to undefined) 
  |*![null, false, myVar]> one(^) // the same as above but abort the pipe
  |?[myVar, 34]> one(^) // execute only if value matches the listed values, otherwise skip
  |*[myVar, 34]> one(^) // execute only if value matches the listed values, otherwise abort the pipe
@ljharb
Copy link
Member

ljharb commented Sep 9, 2021

This seems much more complex than something like:

x |> (^ == null ? no(^) : yes(^))

@jamiebuilds
Copy link
Member

I could see a use case for bailing out of pipeline early.

// pseudo-syntax
let value = a()
  |> ^ == null ? BAIL_KEYWORD : b(^)
  |> c(^)

I don't know if anything like that exists in other languages around pipelines or similar constructs.

@tabatkins
Copy link
Collaborator

tabatkins commented Sep 9, 2021

The closest we have to a "bail-out option" is optional-chaining itself, where null/undefined serve that purpose and short-circuit the rest of the property/method chain. With optional-chaining existing for property access (both dot and bracket) and function calls, it makes sense to me to spread that syntax to pipes, so that a null/undefined topic would immediately bail on the rest of the pipeline and just return null/undefined, identical to a.?b.c.d when a is null/undefined. This is already covered by #159.

The new thing introduced by this thread is the idea of a pipe operator that just skips a single pipeline step if the topic is null/undefined and continues to the next. I'm moderately opposed to this, I think:

  • Pipe bodies can actually do something with a null/undefined, so this definitely could make sense (unlike the same in other optional-chaining cases, where you can't get any key off of a null/undefined, so skipping one won't make the next suddenly work).
  • But it's still a mild variation on optional-chaining behavior, and small, relatively subtle variations in behavior make for code that's harder to read and reason about. Unless there's a strong use-case for the slight variation, it's usually better to just stick with the one behavior and let people use slightly less convenient syntax for cases it doesn't quite cover. (It also means that's one less combination of ASCII chars we can use for something more distinct.)
  • As @ljharb points out, skipping a single pipeline step is easy to do with existing syntax - you might even be able to just leverage optional-chaining itself, depending on what you're doing: void 0 |> ^.?foo |> console.log(^) will successfully log undefined. This isn't true of the one that acts more like optional-chaining, where you'd have to nest the later steps inside the conditional to achieve it without a special operator.

@aadamsx

This comment has been minimized.

@ljharb

This comment has been minimized.

@aadamsx

This comment has been minimized.

@mAAdhaTTah

This comment has been minimized.

@tabatkins
Copy link
Collaborator

This entire side thread is off-topic. Please keep your comments in this issue to commenting on the issue.

@tabatkins tabatkins added the enhancement New feature or request label Sep 9, 2021
@sdegutis
Copy link

This seems much more complex than something like:

x |> (^ == null ? no(^) : yes(^))

Quick question: Would that inline the topic twice, or assign it to an engine-internal temp variable? Assuming the latter?

@ljharb
Copy link
Member

ljharb commented Sep 16, 2021

@sdegutis that'd be up to the engine needs unobservable by the user.

@js-choi
Copy link
Collaborator

js-choi commented Sep 16, 2021

I just realized that the |?> example in the explainer doesn’t short-circuit. I meant for it to short-circuit, and I’ll fix that later.

@sdegutis
Copy link

sdegutis commented Sep 16, 2021

@sdegutis that'd be up to the engine needs unobservable by the user.

The semantics of it are pretty important. If topic is simply expanded, and the computation is expensive (e.g. an API call), then inlining topic multiple times would be a serious problem and a show-stopper in many cases. If it's necessary to guarantee single execution of LHS regardless of topic instances in RHS, this should be in the spec.

@ljharb
Copy link
Member

ljharb commented Sep 16, 2021

@sdegutis obviously it wouldn’t be permitted to run any computation more than once, in any scenario. It’s not a macro.

@sdegutis
Copy link

Got it, thanks. To me it wasn't exactly obvious, so clarification was appreciated.

@tabatkins
Copy link
Collaborator

Yeah, the LHS is evaluated, and the result is then bound to the placeholder.

The explainer does indeed not make this quite clear; I'll fix. The proposed spec text is clear, tho.

@js-choi js-choi added documentation Improvements or additions to documentation follow-on proposal Discussion about a future follow-on proposal labels Sep 19, 2021
@js-choi js-choi removed the documentation Improvements or additions to documentation label Sep 19, 2021
@thi-marques
Copy link

thi-marques commented Jan 23, 2023

I'm a bit concerned about the font ligature breaking with the question mark between the "|>" characters

Optional hack pipe font ligature

@mlanza
Copy link

mlanza commented May 20, 2024

This is a good proposal in at least being able to short-circuit a pipeline which returns null or undefined at some point.

I have been using _.chain(x, f, g, h, i) where x is an input, and f thru i functions for normal pipelines for years. But beyond that I have also been using _.maybe(x, f, g, h, i) too, which is similar but aborts once null or undefined is encountered and returns that null or undefined. I use it a fair bit. That is, out of all the pipelines I write perhaps 20-40% are maybe pipelines.

I'll add one thing, since you are short-circuiting primarily on undefined. I short-circuit on either null or undefined in my maybe pipeline, since invoking something on null leads to exceptions, too.

const A = x 
  |?> f 
  |?> g 
  |?> h 
  |?> i //short-circuit on undefined
const B = x 
  |.> f 
  |.> g 
  |.> h 
  |.> i //short-circuit on null
const C = x 
  |?.> f 
  |?.> g 
  |?.> h 
  |?.> i //short-circuit on undefined/null
const E = (x 
  |.> f 
  |.> g 
  |.> h 
  |.> i) || "foo" //short-circuit on null returning a default
const F = x 
  |> f 
  |.> g 
  |.> h 
  |.> i 
  |> j
  |> k //interleave short-circuit and normal pipelines
const G = x 
  |> f 
  |.> g 
  |.> h 
  |.> i 
  |> either(?, "foo") //given interleaving, return a default of one's choosing

Where a short-circuit pipeline meets a normal, it just passes the value thus far (null/undefined) to the next operation in the pipeline. As example f demonstrates, it's easy to designate some default return value on a short-circuited operation.

I don't care what the characters are, just that the option for short-circuiting exists on null/undefined. It's a totally sensible use case and one I use regularly.

I know that the overall proposal is stuck and has been for years. So this is more of an add-on, which I hope will be contemplated, since knowing what's potentially next can help direct the current design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request follow-on proposal Discussion about a future follow-on proposal
Projects
None yet
Development

No branches or pull requests

10 participants