dead-claudia/lifted-pipeline-proposal

Alternate operator choices

gabejohnson opened this issue ยท 22 comments

Opening an issue to track operator suggestions

@isiahmeadows I know you're familiar w/ |> though it has slightly different semantics.

I also like ~> and <~.

Overloading >>> would let you have that and <<< (ala PureScript). This is probably not tenable.

As for each of your suggestions:

  • I would be okay with |>, because there's numerous other operators available for that and a very similar proposal, including :: (used by the latter) and ->.
  • I'd have to say no to ~> and <~, because the latter conflicts with a<~b โ†” a < ~b, and would have 0% chance of consideration.
  • Overloading >>> would likely fail because of the x >>> 0 cast to unsigned in asm.js, and operator overloading in general has been a hard sell to implementors because of performance concerns.

Another idea I just had was this: f :> g and g <: f (i.e. dropping the first-ish character).

I think we should stick with |> and <| unless there is a really good reason not to. Simply because that syntax has already been adopted by other languages. But if there is ambiguity, I like @isiahmeadows :> <: option too.

Great write up btw @isiahmeadows in the readme.

@JAForbes Neither <:/:> nor <|/|> conflict with anything. I was just thinking that :> and <: are easier to type on a keyboard (you don't have to reach far to the right away from all the letters to type the operator).

What about <<, like Elm does? It feels easier to type.

@charliesbox << is already taken. It's the bitwise shift operator.

oh bummer, you're right. I guess <<< like others said will do the trick :)

Just have a look on other languages:

Language Expression
Haskell f . g
SML f o g
F# f >> g, f << g
Perl 6 f โˆ˜ g (โˆ˜ is U+2218)
Java 8 ((Function<T1, T2>)f).compose(g)

Personally prefer a method instead of operator. Operator just making ES more and more complex.

  • Function.combine
  • Function.prototype.combine

Both are seems better than an operator to me.

considering that pipe is gonna be an operator for sure, I would say that composition should be an operator too just to be consistent.

My two cents.

https://github.com/gilbert/es-pipeline-operator

Yeah I agree, but I think the roll out of these things will be more successful if we stage it. That linked gitter thread covers my thoughts on this @charliesbox

Plus, it's easier to optimize. There's a few complicating factors for transpilers specifically, though (requiring a bit more desugaring boilerplate), but it'd be nothing for engines:

  • new (F :> g)(...args) is equivalent to g(new F(...args)).
  • (f :> g)(...args) is equivalent to g(f(...args).
  • (f :> g).length === f.length always evaluates to true provided no errors are thrown, f is not a proxy, and f.length is not a getter.

Engines could do some seriously crazy stuff with it, though, even at the bytecode level:

  • Proxy argument ICs to the caller function, return ICs from the returning function
  • Treat chain holistically for IC handling. (minor JIT boost)
  • Inline lambdas' bytecode into the composed function at bytecode compile time, only allocating enough to produce its stack frame at creation time (mild memory savings, mild creation time savings, major JIT boost)

The thing is, with a function instead, you have to wait for the JIT to kick in to detect the last one, and engines aren't likely to want to duplicate bytecode. This makes it much harder to derive any sort of special optimization, but by making it easily statically analyzable, there's fewer moving parts involved. In addition, transpilers could do the latter if it were syntax (offering most of the benefit up front), but not if it were just a function.

In addition, transpilers could do the latter if it were syntax (offering most of the benefit up front), but not if it were just a function.

I don't understand why that has to be true. Is there an assumption that someone might have changed the behaviour of the function so we can't special case Function.pipe when optimizing?

I guess I can understand that pessimism in the JS engine itself (even if I think its misguided). But with transpilers/build tools - that's a tool devs opt in to, and if you can identify that the native pipe function is being invoked, you could certainly inline many compositions.

And if this is the major reason for preferring syntax. Why can the proposal prevent reassignment (writeable=false), so we can guarantee expressions like Function.pipe are exactly what the JS engine thinks it is.

FWIW I think additionally having syntax could be a good thing (down the line). But could someone clarify this, because I keep hearing this argument about syntax being faster/easier to optimize and I don't understand it at all.

@JAForbes

And if this is the major reason for preferring syntax.

Transpilers are more of an added bonus, but it would take a while as usual for implementors to get on board with anything they didn't themselves invent (like SharedArrayBuffer/Atomics), even with stuff they like.

Why can['t] the proposal prevent reassignment (writeable=false), so we can guarantee expressions like Function.pipe are exactly what the JS engine thinks it is.

It wouldn't exactly fit in with the rest of the spec that way, and that kind of thing would arise out of implementor feedback (why Symbol.iterator and friends are non-writable, non-configurable).

FWIW I think additionally having syntax could be a good thing (down the line). But could someone clarify this, because I keep hearing this argument about syntax being faster/easier to optimize and I don't understand it at all.

It really comes down to the fact if it's syntax, implementors could optimize statically with a few heuristics to ensure it's zero-cost and cheaper to create. If it were a function instead, you'd have to check if each argument is a lambda, and you could only do it when the JIT fires (so you couldn't avoid the overhead of an entire lambda).

When calling the composed function, it's much closer to identical, but here's the difference:

  • Syntax: Call the composed function, mostly as if it were a normal function, and return the result.
  • Function: Call the first function, and recursive pipe the result through the remaining functions for a new result.

There's another benefit to just syntax: you don't have to cover the case of zero or one arguments, which the concept isn't well-defined in (at least mathematically). If you need to compose a list of functions, you could do it this way:

  • Syntax: funcs.reduce((f, g) => f :> g)
  • Function: Function.pipe(...funcs)

It wouldn't exactly fit in with the rest of the spec that way

The language spec do you mean? Yeah that's fair, but I think its worth considering very seriously because it solves a lot of problems and it doesn't "break the web"

It really comes down to the fact if it's syntax, implementors could optimize statically with a few heuristics to ensure it's zero-cost and cheaper to create. If it were a function instead, you'd have to check if each argument is a lambda, and you could only do it when the JIT fires (so you couldn't avoid the overhead of an entire lambda).

When you say lambda, do you mean arrow function? Is that so the compiler can identify the complete source of the function easily? Because I think the modules spec makes that a non issue, even for libraries. We should plan for what the language will be by the time this lands, as opposed to the confusing modules situation we have right now.

If we have a modules spec, then we can statically determine where a function came from and inline it no matter what its source is. Or at least attempt it in 99% of cases.

and you could only do it when the JIT fires

Does what I just said negate that? It should be 100% possible at parse time right?

When calling the composed function, it's much closer to identical, but here's the difference:
Syntax: Call the composed function, mostly as if it were a normal function, and return the result.
Function: Call the first function, and recursive pipe the result through the remaining functions for a > new result.

See I'm imaging both cases would be very different to that. Function.pipe (or syntax) could inline the entire composition into one function at parse time. Then when it executes, it's just executing 1 function. Whether its syntax or not right?

There's another benefit to just syntax: you don't have to cover the case of zero or one arguments

That's true, but that's sort of an edge case that I wouldn't want to use as a guiding principle for a spec design.

So with all that said, is there any advantage to syntax? Am I still misunderstanding something?

@JAForbes

When you say lambda, do you mean arrow function? Is that so the compiler can identify the complete source of the function easily? Because I think the modules spec makes that a non issue, even for libraries. We should plan for what the language will be by the time this lands, as opposed to the confusing modules situation we have right now.

By "lambda", I mean any function expression. Also, engines could also broaden that to single-use functions, but that's a separate deal.

Does what I just said negate that? It should be 100% possible at parse time right?

See I'm imaging both cases would be very different to that. Function.pipe (or syntax) could inline the entire composition into one function at parse time. Then when it executes, it's just executing 1 function. Whether its syntax or not right?

Not necessarily, even if it's non-writable, non-configurable: what if the global Function is redefined before the script is executed, or conditionally overwritten at initial run time before the call? Engines compile before globals are known, and they create their ICs lazily to eventually speculatively optimize for the case everything is as you would expect.

Also, remember that the global object itself is a normal object with all its properties configurable, enumerable, and writable.

That's true, but that's sort of an edge case that I wouldn't want to use as a guiding principle for a spec design.

It makes things easier to explain to newbies.

So with all that said, is there any advantage to syntax? Am I still misunderstanding something?

There's also the aesthetic side, with fewer parentheses, but that's more of a style thing.

I'd personally like to put my 2ยข in and say that I am a huuuuge fan of PureScript's <<< and >>> syntax. To have that replicated in JS would just be brilliant as far as I'm concerned.

</ยข2>

@MiracleBlue >>> is out of the question as that's JS's unsigned logical shift right operator. (And obviously, functions can be coerced to integers that way.)

Damn :( that is unfortunate. I didn't realise it was already used for something in JavaScript. My apologies.

@MiracleBlue To be fair, I would've liked >>/<<, except those are both taken.