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

Question about ^ when a "child pipeline" is present. #208

Closed
benlesh opened this issue Sep 13, 2021 · 29 comments
Closed

Question about ^ when a "child pipeline" is present. #208

benlesh opened this issue Sep 13, 2021 · 29 comments

Comments

@benlesh
Copy link

benlesh commented Sep 13, 2021

RxJS plans to move toward whatever pipeline operator lands. It's very frequent in RxJS that we have child observables that have been set up with pipes of their own. The pattern below is very common:

clicks$.pipe(
  concatMap(() => getData$.pipe(
    catchError(err => {
       console.error(err);
       return EMPTY;
    });
  )),
  map(result => result.someData),
)
.subscribe(console.log);

I'm not sure how to cleanly handle this with Hack pipeline:

clicks$
  |> concatMap(() => getData$
    |> catchError(err => {
        console.error(err);
        return EMPTY;
    })(^) // <-- in particular, I'm not sure what will go here?
  )(^)
  |> ^.subscribe(console.log)

It seems like the presence of a |> in a child function would give new meaning to the ^ on its RHS until the end of the function's context?

I've seen in other examples that ^ can be used within closures. Which is why I have this question.

@mAAdhaTTah
Copy link
Collaborator

It seems like the presence of a |> in a child function would give new meaning to the ^ on its RHS until the end of the function's context?

This is correct: the |> operator introduces a new expression scope, so the (^) on line 6 refers to what you expect it to.

@benlesh benlesh closed this as completed Sep 13, 2021
@benlesh
Copy link
Author

benlesh commented Sep 13, 2021

Thanks, that's what I expected. It's a little confusing to read in the example above, but that's a real example. It's a shame the inner ^ can't be named differently.

@benlesh
Copy link
Author

benlesh commented Sep 13, 2021

Actually, I'll leave this open. As it's probably worth discussing with the committee how to handle this confusion? Are there any possible ways to rename this? Any ideas around how to make this more readable? @tabatkins

@benlesh benlesh reopened this Sep 13, 2021
@ljharb
Copy link
Member

ljharb commented Sep 13, 2021

I think it's the same readability concerns whenever any chains get "too much stuff" shoved into it - the solution in my experience tends to be "de-inline stuff as needed". In this case:

const mapper = () => getData$
    |> catchError(err => {
        console.error(err);
        return EMPTY;
    })(^);
    
clicks$
  |> concatMap(mapper)(^)
  |> ^.subscribe(console.log)

@tabatkins
Copy link
Collaborator

Yeah, the general solution is "don't chain that much"; you're over-nesting in an operator meant to reduce nesting. ^_^

That said, if this is necessary, do-expressions will solve the problem; you can assign the outer pipe's topic to a variable inside the do-expr and then access that var in the inner pipe. I do expect do-exprs to eventually land (we've been circling them for years, but the champions are doing good work at cleaning up the remaining issues), so I don't think pipeline has to particularly worry about this issue on its own.

@tabatkins
Copy link
Collaborator

tabatkins commented Sep 14, 2021

That said, looking back at the example in the OP, I think it's fine? It doesn't use an arrow func to capture the piped topic at any point, so it's still relying on implicit topic-passing, and thus is directly convertable into Hack-style with the naive transformation.

// original
clicks$.pipe(
  concatMap(() => getData$.pipe(
    catchError(err => {
       console.error(err);
       return EMPTY;
    }),
  )),
  map(result => result.someData),
)
.subscribe(console.log);

// to...
clicks$ 
	|> concatMap(()=>getData$ 
		|> catchError(err => {
			console.error(err);
			return EMPTY;
		})(^) // <-- close catchError, pass getData$
	)(^) // <-- close concatMap, pass clicks$
	|> map(result => result.someData)(^) // <-- concatMap() result
	|> ^.subscribe(console.log);

The extra parens aren't great to read, I'll grant you, but if the functions are written to take their data as an argument it's a lot better:

clicks$ 
|> concatMap(^, ()=>getData$ 
	|> catchError(^, err => {
		console.error(err);
		return EMPTY;
	}))
|> map(^, result => result.someData)
|> ^.subscribe(console.log);

And yeah, as @ljharb says, if you get serious with "reduce nesting" this reads fairly cleanly even with pure "pass topic as separate call":

function logDataError() {
	return getData$
	|> catchError(err=> {
		console.error(err);
		return EMPTY;
	})(^);
}

clicks$
|> concatMap(logDataError)(^)
|> map(result=>result.someData)(^)
|> ^.subscribe(console.log);

But going that far isn't necessary if prefer the complexity in the first step.

@Avaq
Copy link

Avaq commented Sep 14, 2021

Also note that with the Hack pipelines, if you have a single step, it's probably nicer just to inline the input instead:

-getData$ |> catchError(^, err => {
+catchError(getData$, err => {
   console.error(err);
   return EMPTY;
 })

I guess the same is true for when the RHS is curried:

-getData$ |> catchError(err => {
+catchError(err => {
   console.error(err);
   return EMPTY;
-})(^)
+})(getData$)

That makes this specific example a bit less cluttered:

clicks$
  |> concatMap(() => 
    catchError(err => {
      console.error(err);
      return EMPTY;
    })(getData$)
  )(^)
  |> ^.subscribe(console.log)

@tabatkins
Copy link
Collaborator

Yeah, I thought about that, but for the sake of the discussion didn't want to change the structure of the example.

@ken-okabe
Copy link

ken-okabe commented Sep 14, 2021

In fact, this is a good little example.

The history of software development is fight against complexity, and various attempts have been done.
Object Oriented Programming is the one and failed against the complexity.
The fundamental reason to fail is that people invent something with assumptions that can only adopt shallow level structures.

OOP class is the one.
React once employed OOP class, and they admitted the failure and had to discard OOP class from their framework.
React or VirtualDOM framework has essentially nested structures, and OOP class has been designed with assumptions of non-nesting structures, that is why they failed.

RxJS is a relatively clean framework, and mostly because they employ clean Functional Programming, and enjoyed the robustness of the simplicity.
Now, RxJS is told to be

Yeah, the general solution is "don't chain that much"; you're over-nesting in an operator meant to reduce nesting. ^_^

This is an unfortunate mention to express the fact Hack-style pipeline-operator is not robust against this shallow level of complexity.

OOP class fails to prove robustness in React framework.
Hack pipeline-operator fails to prove robustness in RxJS.

I expect RxJS will be a very hard framework to code in future. I don't think users can follow this confusion.

I've been a FP programmer for years, and F# style pipeline-operator helped me to write code so much easier and lot of freedom obtained.
https://github.com/stken2050/io-next/blob/master/code/src/io-next.ts

F# is based on math, and the pipeline-operator is also pure math operator which is essentially robust against complexity.
Operator is a term of algebra, and as long as we stick to Math/algebra, we can safely avoid the problem of complexity of software development unlike artificial OOP class design failure.

That said, if this is necessary, do-expressions will solve the problem;

So, after inventing a new artificial mechanism that is not essentially math-operator, the new problem has emerged, and in order to cover up the problem we need to introduce new do...

Please note that do is also not a math-operator. It's not an expression but a statement. Outside of the math structure another artificial mess emerges.

It seems like the presence of a |> in a child function would give new meaning to the ^ on its RHS until the end of the function's context?

This is correct: the |> operator introduces a new expression scope, so the (^) on line 6 refers to what you expect it to.

Yes.

microsoft/TypeScript#43617 (comment)

Personal opinion: The hack-style pipeline operator is bad because it introduces a new context variable, like this, in the form of #. These don't nest in intuitive ways. In addition, the operator doesn't actually save much space for many usages. You could rewrite....

this is a product of OOP, and the artificial design failed to sustain robustness against software complexity. We the programmers-human can't control the full of context variables, and now again, Hack-style-pipeline operator provide us nesting context.

FP or Functional operator or Math algebra operator or F# style operator never has context. Context is the fundamental sin of the complexity that we the programmers-human never be able to handle. Bug reason.

IF we smartly have a function-composition-operator such as . in addition to FP style |>

a |> f |> g
equals
a |> (f . g)

  • I usually define the composition operator as f.g not g.f

I can teach FP beginners in my book based on Math/algebra.
This is essentially independent of language specification. You can learn the concept from Haskell,Scala, or F# tutorial book because Math/algebra is common.

In Hack,
a |> f(^) |> g(^)
equals
a |> (f . g)(^)

This is not math, and lose simplicity and robustness against the complexity.

What is ^ ? What about context? How can I teach to beginners?
Will they adopt? I don't think so.

@tomByrer
Copy link
Contributor

In Hack,
a |> f(^) |> g(^)
equals
a |> (f . g)(^)

I don't understand why this is bad? So what if programmers can mix & match method chaining & Hack-pipelines? Some codebases mix together OO & Functional Programming. I agree mix & matching styles isn't ideal, but happens.

Perhaps you could write an ESLint rule to warn against mixing chaining & pipes? This would be helpful for beginners & experts who are transcribing into pipes.

@ken-okabe

This comment has been minimized.

@nicolo-ribaudo

This comment has been minimized.

@ken-okabe

This comment has been minimized.

@ljharb

This comment has been minimized.

@js-choi

This comment has been minimized.

@ken-okabe

This comment has been minimized.

@js-choi

This comment has been minimized.

@tabatkins
Copy link
Collaborator

I'm going to skip over the significant digression about math; that's not directly relevant to this topic. But:

This is an unfortunate mention to express the fact Hack-style pipeline-operator is not robust against this shallow level of complexity.

As I said in the comment this was responding to, the point of the pipeline operator is to linearize code and reduce nesting. It is not meant as a general-purpose code-flow operator meant to chain together large amounts of code. It can be misused as such, sure, but it being somewhat painful to do is an accidental benefit; JS has many good ways to organize code in a more readable fashion, and when those are used it's easy to write pipelined code without issues.

That said, it is also still true that Hack-style and F#-style pipelines are precisely identical in power (ignoring await/etc issues); if one wants to write a nested expression that uses more pipelines, and mix mention of the outer topic and inner topic together, one can do so with an arrow function, identically in either syntax. You just have to immediately invoke it in Hack-style, whereas it's implicitly invoked in F#-style (the "put a (^) at the end" thing again).

@ken-okabe

This comment has been minimized.

@ken-okabe

This comment has been minimized.

@tabatkins

This comment has been minimized.

@ken-okabe

This comment has been minimized.

@HKalbasi
Copy link

Perhaps we need to uncurry functions and libraries after hack pipe?

So OP example would become:

clicks$
  |> concatMap(^,() => getData$
    |> catchError(^, err => {
        console.error(err);
        return EMPTY;
    })
  )
  |> ^.subscribe(console.log)

I think it is now clear? Uncurring will be create a temporary turmoil in functional js ecosystem, but after some years we will probably adopt it.

@tabatkins
Copy link
Collaborator

@HKalbasi Right, I gave that as one of the options in #208 (comment). And note that the majority of the JS ecosystem is already "uncurried". ^_^

@stken2050 I'm sorry, but you're wrong. The monad laws cover how the bind/return monadic operators must work. The F#-style pipe operator can be thought of as the map operator of the Function functor. It is absolutely not the monadic bind operation, over the Function monad or any other.

Regardless, this is a great digression from the topic of this thread, which was covering nested pipes and the effect on the ^ placeholder.

@ken-okabe

This comment has been minimized.

@ken-okabe
Copy link

ken-okabe commented Sep 16, 2021

I think it is now clear? Uncurring will be create a temporary turmoil in functional js ecosystem, but after some years we will probably adopt it.

This is what we are afraid of... Never.
Currying is the natural consequence of functional programming. Look at Haskell.
There are solid reasons we use curried unary functions, and the reason stands on not "js specification" but mathematics.
This will destroy functional js ecosystem forever not temporal, and we will never be able to adopt it.

@lightmare
Copy link

Assciativity and Left&Right identiry laws are satisfied.

No, the F# pipe operator transplanted into JS is not associative, because rearranging parentheses changes meaning.

(x |> f) |> g /* desugars to */ g(f(x))
x |> (f |> g) /* desugars to */ (g(f))(x)

@sdegutis
Copy link

clicks$.pipe(
  concatMap(() => getData$.pipe(
    catchError(err => {
       console.error(err);
       return EMPTY;
    });
  )),
  map(result => result.someData),
)
.subscribe(console.log);

I really can't help but think maybe there's another kind of pattern library that's waiting to be born from this kind of thing. I've felt that way every time I've seen RxJS code or tried to use it myself, and especially feel that way using this example. It intuitively feels to me like Hack Pipes will make such a new library a lot more realistic than ever before. I don't have any specific examples right now, it's just, on the tip of my mind so to speak.

@tabatkins
Copy link
Collaborator

I'm going to go ahead and close this issue; it's gotten way off track.

As far as I can tell, the original issue raised by @benlesh has been addressed as well: do-exprs, when they mature, will allow easy renaming of an outer topic so it doesn't clash with nested pipelines. While it's slightly less convenient syntax-wise, an IIFE also does the job, at the cost of preventing async/yield/etc in the inner pipe. Finally, there is discussion focused on the possibility of topic-renaming going on in #203.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 11, 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