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

Alternate syntax #4

Closed
gilbert opened this issue Dec 6, 2015 · 55 comments
Closed

Alternate syntax #4

gilbert opened this issue Dec 6, 2015 · 55 comments
Labels

Comments

@gilbert
Copy link
Collaborator

gilbert commented Dec 6, 2015

Although I wrote the proposal, I do recognize a potential objection: functions with multiple arguments.

Although to me using arrow functions is perfectly explicit and clear, for the sake of argument let's explore two possible alternatives to handling this situation.

1. Elixir-style Inlining

Elixir's pipeline operator is actually different than the original proposal you see in this repo (of which we will call F# style). Instead of simply calling the right-hand side with the left-hand side, it instead inserts the left-hand side as the first argument to the right:

// F# style
run("hello") |> withThis(10);
// is equivalent to
withThis(10)( run("hello") );

// Elixir style
run("hello") |> withThis(10);
// is equivalent to
withThis( run("hello"), 10 );

//
// More complicated example
//
// F# style
run("hello") |> withThis(10) |> andThis(20);
// is equivalent to
andThis(20)( withThis(10)( run("hello") ) );

// Elixir style
run("hello") |> withThis(10) |> andThis(20);
// is equivalent to
andThis( withThis( run("hello"), 10 ), 20 );

Pros / Cons

  • 👍 Less function calls (2 vs 3)
  • 👍 Compatible with much more of the JavaScript ecosystem
  • 👎 Gives a new semantic meaning to what looks like a normal function call. For example, withThis(10) is no longer just calling that function with one argument
  • 👎 Putting the left-hand side as the first argument to the right-hand side is pretty arbitrary.

2. Placeholder Arguments

Another alternative is to have a new syntax for "placeholder arguments" – basically, slots waiting to be filled by the next function call. For example:

// Placeholder style
run("hello") |> withThis(10, #);
// is equivalent to
withThis( 10, run("hello") );

//
// More complicated example
//
run("hello") |> withThis(10, #) |> andThis(#, 20);
// is equivalent to
andThis( withThis( 10, run("hello") ), 20 );

Pros / Cons

  • 👍 Very explicit (no surprises)
  • 👍 Less function calls
  • 👍 Compatible with even more of the JavaScript ecosystem
  • 👎 Requires more new syntax
  • 👎 Usage of the hash operator (#) would probably be hard to define outside coupled use with pipeline operator (this problem could be avoided by simply making it illegal)

Any thoughts and feedback would be greatly appreciated. Even if you are reiterating some of the points I've made, it's important to express them so we can see what the community thinks. Thanks for reading.

@acdlite
Copy link

acdlite commented Dec 6, 2015

My initial impression is that these alternatives don't quite gel with how function application usually works in JavaScript. They make more sense in languages that have currying.

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 6, 2015

@acdlite I agree with you with respect to the Elixir-style syntax; what looks like a function call is no longer just a function call. However, I think the placeholder-style does look distinctly different enough to give the impression, "something else must be going on semantically, now".

@eplawless
Copy link

If it's necessary to extend to multi argument (I agree that arrow functions are easy to understand here) the # placeholder does make it clear that something new is happening.

What happens if you put two placeholders?

What do nested pipeline operators do with placeholders?

@seanstrom
Copy link

@mindeavor have you looked at LiveScript? It also has support for the |> operator and also implements the placeholder arguments.

@AriaMinaei
Copy link

Expanding on @mindeavor's comment, LiveScript has a simple syntax for partial application: add(1, 2) == add(1, _)(2).

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 6, 2015

@eplawless Great points, this is something that would need to be worked out (assuming people are really against just using arrow functions).

@seanstrom @AriaMinaei Thank you for the links to LiveScript's prior art, it's good to know that others already find the |> operator useful. I would also love to have underscore as partial application, but as you and I know, underscore is a commonly used variable name in JS :)

@AriaMinaei
Copy link

@mindeavor How about dashes? add(1, -, -) As you see, they can be funny too :)

@seanstrom
Copy link

@eplawless

What happens if you put two placeholders"

Do you mean what happens if we do:

run("hello") |> withThis(1, #, #)

?

What do nested pipeline operators do with placeholders?

Can you provide an example of what you mean?

@seanstrom
Copy link

@mindeavor yeah underscores could confused things, but LiveScript also implements the placeholder arguments in a way that doesn't reserve the _ character. So doing _.map would still work.

@dralletje
Copy link

@seanstrom although the moment you actually want to pass underscore for some reason, you'd have to do it with another name. It would make it harder too debug, I think

@seanstrom
Copy link

@dralletje correct, I'm not sure if anyone has brought that to the LiveScript teams attention. It's probably one of those things where most LiveScript users use the prelude.

@eplawless
Copy link

@seanstrom
Please excuse any errors, I'm about to go forage for coffee.

Yes, your example is what I meant. Should the result be stored in a temporary and passed in both places? If so, can that violate the normal order of operations? Should "run" be executed twice, instead?

Here's what I mean by nested pipeline operators:

1234 |> (5678 |> bar(#))

I assume it binds to its nearest ancestor pipe operator in the AST (so, the second one). Are there any problems with that?

@mindeavor
Do you know what the operator precedence of |> should be relative to . ?

@seanstrom
Copy link

Strictly going off of LiveScript,

Something like this:

const add = (x, y, z) => x + y +z
var add3 = 1 |> add(2, #, #) // add(2, 1, #)
var result = add3(3)

So the left hand of operator would go into the first # argument slot. Since that function would still need one more argument, the piping expression would result in a function that takes that remaining argument and once received it would apply the whole thing.

Nested expressions like this:

3 |> (1 |> add(2, # , #))

Would most likely be evaluated by precedence from the parens from left to right I believe.
So we would first evaluate (1 |> add(2, #, #)) and then we would apply 3 |> add(2, 1, #).
Which is what I believe you were mentioning right?

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 6, 2015

Do you know what the operator precedence of |> should be relative to . ?

I imagine the |> would have the lowest operator precedence, which would mean the . operator would happen first.

@dvlsg
Copy link

dvlsg commented Dec 6, 2015

I agree that the arrow functions are explicit and clear. They seem like the most intuitive option to me, and would also allow for easy destructuring where necessary (assuming it would be necessary - I suppose the piped functions could handle accepting objects / iterables and destructure internally, as well).

@Havvy
Copy link

Havvy commented Dec 7, 2015

Note that function piping is just syntactic sugar, so we could bikeshed on it all day.

Personally, I like the Elixir version, but then, I also write Elixir. The LiveScript version looks like it'd work too, but I'd rather see a keyword there rather than an _ or #. As this is new syntax, it should be relatively easy to add a new keyword. Sort of like how let works.

@jasmith79
Copy link

What about restricting use to unary functions? Then onus would be on the programmer to partially apply, would eliminate a lot of potential cognitive overhead.

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 7, 2015

@jasmith79 I agree, in fact that is the original proposal :)

@MiracleBlue
Copy link

I'm super, super excited about having a pipeline operator, but to just throw my two cents in here, I'd prefer the Elixir-style syntax. I think the pipeline itself semantically describes enough of what is going on. I don't feel the need to be super explicit about where the left-hand value is going. It also makes it cleaner visually. The only concern obviously would be learning curve for new users (especially ones not used to that kind of magic).

That's just my two cents, though. Take it or leave it :)

@Havvy
Copy link

Havvy commented Dec 7, 2015

Another thing that could be done is waiting for a macro system in JS in the
future, and letting the pipe syntax just be a macro.

On Mon, Dec 7, 2015 at 2:07 PM, Nicholas Kircher [email protected]
wrote:

I'm super, super excited about having a pipeline operator, but to just
throw my two cents in here, I'd prefer the Elixir-style syntax. I think the
pipeline itself semantically describes enough of what is going on. I don't
feel the need to be super explicit about where the left-hand value is
going. It also makes it cleaner visually. The only concern obviously would
be learning curve for new users (especially ones not used to that kind of
magic).

That's just my two cents, though. Take it or leave it :)


Reply to this email directly or view it on GitHub
#4 (comment)
.

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 7, 2015

Perhaps more fuel to the fire: it looks like someone made an Elixir-style pipe operator pull request to CoffeeScript jashkenas/coffeescript#4144

It seems to me the programming community at large is moving towards this functional programming direction.

@jamen
Copy link

jamen commented Dec 8, 2015

Perhaps a different function definition to handle piped data (e.g. generators are function*) such as function^, function~, or whatever.

From there, piped data could be accessed through a special keyword (or maybe this?):

const example = function~(arg) {
  return this + arg;
};

let test = 10
  |> example(5);

// test === 15

This is similar to how a native JavaScript object prototype methods would work, using the this keyword to access the data it's being called on.

@barneycarroll
Copy link

@jamen that's the ES7 bind operator.

let test = 10::example( 5 )

Non-arrow functions can already have this applied, so plain old ES5 functions work fine.

@jamen
Copy link

jamen commented Dec 8, 2015

@barneycarroll, yeah that's a good point (I didn't know about the bind operator). Now that I think about it, there isn't really a way to use arrow functions with my method.

However I'd like to bring up @mindeavor thoughts in the original post:

... to me using arrow functions is perfectly explicit and clear, ...

Arrow functions work perfectly then imo. Is there a reason to add alternative ways to use it if we already have a clear-cut way?

const example = data1, data2 => {
  return data1 + data2;
};

let test = 10
  |> x => example(x, 5);

// test === 15

This seems perfectly fine. There is no abstraction because you know what data is going where, and there is no new syntax besides the pipe operator (which is a given).

If you really really needed a shorthand syntax to eliminate the arrow function out of each pipe, you could just "double wrap" your function (or a different function) to return another arrow function holding the piped data:

// My function:
const example = data => pipe => data + pipe;

// Converting another function into a pipe-able function:
const pow = data => pipe => Math.pow(pipe, data);

let test = 10
  |> example(5);
  |> pow(2)

// test === 225

I personally don't think it's good to "sugarcoat" standards when you can have a perfectly working method that's already in the current syntax.

However I do understand the initial concern, I just don't think it's that big of a deal.

@MiracleBlue
Copy link

My only concern with that is it's diverging from what many other languages
already do. I'm always in favour of conforming to the common pattern where
it makes sense.

On Tue, Dec 8, 2015 at 12:21 PM, Jamen Marz [email protected]
wrote:

@barneycarroll https://github.com/barneycarroll, yeah that's a good
point (I didn't know about the bind operator either). Now that I think
about it, there isn't really a way to use lambdas with my method.

Although to me using arrow functions is perfectly explicit and clear,

Arrow functions work perfectly then imo. Is there a reason to add
alternative ways to use it if we have a clear-cut way?

const example = data1, data2 => {
return data1 + data2;
};
let test = 10
|> x => example(x, 5);

This seems perfectly fine. There is no abstraction, you know what data is
going where, and there is no new syntax besides the pipe operator.


Reply to this email directly or view it on GitHub
#4 (comment)
.

@jamen
Copy link

jamen commented Dec 8, 2015

@MiracleBlue, that's a good point as well. I suppose if there isn't really a big issue to begin with (at least in my opinion), we can make it similar to how it would be done in another language, to limit the learning curve I suppose.

@MiracleBlue
Copy link

Yeah I feel like that makes the most sense. At the very least, if you're
coming from a language which already has this implemented, it makes it a
bit easier.

On Tue, Dec 8, 2015 at 12:45 PM, Jamen Marz [email protected]
wrote:

@MiracleBlue https://github.com/MiracleBlue, that's a good point as
well. I suppose if there isn't really a big issue to begin with (at least
in my opinion), we can at least make it similar to how it would be done in
another language, to limit the learning curve I suppose.


Reply to this email directly or view it on GitHub
#4 (comment)
.

@seanstrom
Copy link

How does the Elixir way and the proposed Coffeescript way handle curried functions? Or any time we have a function that returns a function? For example I may have {} |> buildFn(data) and buildFn is suppose to return a function.

Also, what if I want to point the piped value to a certain argument slot? For example I may have functions that don't want to take the piped data as the first argument. At that point I would have to wrap my function, which is what we would do without the Elixir implementation.

@seanstrom
Copy link

Note that multi-parameters was brought up in the Elixir project elixir-lang/elixir#1156. But it turns out it isn't a priority since the |> is a macro and can be tweaked by the programmer. Unfortunately we're not in the same position. So we need to make sure that we can easily handle:

  • unary functions 10 |> _.identity
  • sort-of partially applied functions 10 |> Math.max(1)
  • functions that return functions 10 |> _.compose(f, g)
  • potentially n-ary functions through placeholders reducerFn |> _.reduce(data, #, {})

Anything that I missed?
Note that place holder arguments can essentially be a sugar towards currying/partially applying a n-ary function.

@jamen
Copy link

jamen commented Dec 8, 2015

potentially n-ary functions through placeholders reducerFn |> _.reduce(data, #, {})

Couldn't we just use a arrow function in this case?

reducerFn 
  |> x => _.reduce(data, x, {})

That would eliminate the need for new syntax.

@seanstrom
Copy link

@robbiespeed
I was talking about a scenario where I have 3 or more argument function and I want to pipe a value to an argument position that isn't the first or the last. For example:

let takesThree = (x, y, z) => x + y * z
1 |> (10, #, 5) |> takesThree // takesThree(10,1,5)

The example uses the # but I did that only to show how I wanted to order the arguments.
As for the partially applied functions, I think we're in agreement on that. If they already ready to curry then just let them right?

@seanstrom
Copy link

@jamen
We could totally use arrow functions in that case, since # would probably be sugar for that.
What's interesting is when you incorporate multiple placeholders. You could say:

reducerFn |> (data |> _.reduce(#, #, {}))

// which is actually

reducerFn |> x => (data |> y => _.reduce(y, x))

@robbiespeed
Copy link

@seanstrom I don't think anyone should use pipe in that case... it's just a single level function call. Can you give me a case with multiple levels that wouldn't work with the proposed syntax?

Modified your example:

function double (x) { return 2*x }
function addThree (x, y, z)  { return x + y + z }
1 |> (10, double, 5) |> addThree // addThree(10,double(1),5)

@jamen
Copy link

jamen commented Dec 8, 2015

reducerFn |> (data |> _.reduce(#, #, {}))

I still think the arrow functions are better. How would the interpreter know what order they're in? If there is no signification and it just defaults to one way and we would have to resort to arrow functions to work around the it anyways.

I think arrow functions are more practical thank placeholders in the long run.

@seanstrom
Copy link

@robbiespeed
I'm not sure what you mean by they shouldn't use |> in that case. It would be for control flow purposes.
In the above example, imagine 1 was a series of piped operations and now I wanted to pipe the result to a function that takes 3 arguments, but as the second argument.

// placeholders
someVal
|> someFn
|> myFn(otherFn, #, {})

// arrow functions
someVal
|> someFn
result |> x => myFn(otherFn, x, {})

// your syntax -- this is what I want to do essentially
someVal
|> someFn
result |> (otherFn, result, {}) |> myFn

@seanstrom
Copy link

@jamen I believe the nearest value being piped would be applied to the nearest open slot.

let add = (x, y, z) => x + y + z
3 |> (2 |> add(1, #, #)) // add(1,2,3)

Though this behavior is mostly based on LiveScript's implementation of placeholder arguments.
If you wanted to re-arrange the arguments you would need a new function, which to your point would mean we could just skip all the extra sugar and just go for wrapper functions. Ideally the placeholder syntax would just be shorthand for currying the function.

@robbiespeed
Copy link

@seanstrom okay so in that example, it seems like you want to pass a function as an argument to a function at the end of the pipe, and that's tricky with my proposed syntax. If we said that wrapping a function in braces made it a reference rather than a function to be applied to the pipe value:

someVal
|> ((otherFn), someFn, {}) |> myFn

or just use arrow functions when we need to pass a function as an argument.

@seanstrom
Copy link

@robbiespeed
You would run into a situation where wrapping something in parens wouldn't mean expression evaluation precedence. For example:

let addWorld = str => str + " World"

someVal |> ((addWorld("Hello") + "!"), someFn, {}) |> myFn
// myFn("Hello World!", someFn(someVal), {})

@robbiespeed
Copy link

@seanstrom thinking about it some more I've realised my syntax would not be able to be parsed easily and optimized to es6. I think something like this would work however:

someVal
|> (otherFn, |> someFn, {}) |> myFn

or if you simply want to pipe the previous value:

someVal
|> someFn 
|> (otherFn, |> , {}) |> myFn

@seanstrom
Copy link

@robbiespeed
Yeah I'm not sure about that syntax. It's definitely a new look/usage of the |> operator for me.
I'm a little pro place-holder arguments because it seems to be vetted by the LiveScript community.
But we could just adhere to using arrow functions for now and possibly adding a sugar syntax later.
I would be willing to handle it that way since I would love having |> in general.

@robbiespeed
Copy link

I'd much rather a keyword than a placeholder. And I find the syntax:
value |> funcA |> funcB(1, result, 2) confusing since typically when calling a function in the pipe we'd expect it to return a function.

Would prefer:
value |> funcA |> (1, result, 2) |> funcB

Not sold on using arrow syntax in place of some added syntax specific to the case. Mostly because I'm not convinced rules could be written to reliably parse it to the equivalent performance of standard nested function calls.

@seanstrom
Copy link

Yeah i'm not convinced a keyword would be worth it, _ in LiveScript is used for partially applying any function with or without the |> operator. It reads well there IMO, but we likely wouldn't be able to convince others to use _. Perhaps there's another character(s) that would somehow convey the intent of a placeholder.

Could you clarify your second statement about parsing the arrow functions? Do you mean the performance between functions with bounded contexts (this) and normal functions?

let funcOne = function (x) { return function (y) { return x + y } }
let funcTwo = x => y => x + y

Like the performance between those two functions? If so I would think that would have to be an optimization by the run-time for when the function does use this or not. Which should mean that we wouldn't need custom rules for parsing things that are known function statements right? We would just leave the implementation up to the programmer. For now they could write the other version inlined or extract the function entirely.

value
|> funcA
|> (function (result) { return funcB(otherStuff, result) })

// ---

let newFn = function (result) { return funcB(otherStuff, result) })
value |> funcA |> newFn

@robbiespeed
Copy link

@seanstrom No I mean in terms of using something like babel to compile the es7 down to es5/6, as well the fact that funcC('fast', funcB(funcA('test'))) would perform better than (x => { funcC('slow', x) })(funcB(funcA('test'))). Ultimately I believe this statement from the proposal is misleading:

Also, because the pipe operator's semantics are pure and simple, it could be possible for JavaScript engines to optimize away the arrow function

Arrow functions allow for some complex things to happen. It would add unneeded complexity to a javascript engine, if it were to first check if the arrow function does complex or simple things, then optimize if it only does simple things. What would be better IMO is to define a syntax where the simple things (or what we can already achieve with nested function calls) are accounted for.

@seanstrom
Copy link

@robbiespeed I believe you're pointing out two things there:

  1. => may be slower than just an ordinary function.
  2. Any extra indirection would be slower than inlining the arguments to the function call.

Is this correct? I just want to make sure I'm on the same page here.
I also now see why the syntax you were suggesting was the way it was, I believe you were trying to represent an easy to parse arguments list right?

@robbiespeed
Copy link

@seanstrom Yes to 2, but not to 1. Depending on implementation arrow functions should be just as fast as regular functions, the problem comes when your wrapping a function call in another function, it's wasted computing.

Yes that was the goal, to represent a clear arguments list for the next function in the pipe to use.

@seanstrom
Copy link

@robbiespeed Cool. I can see why an arguments tuple is better for illustrating that. Normally I would just go through currying the functions and eat the performance cost, since it would be negligible to me. But I can see why the performance cost or the added complexity in engine to optimize that would be a downer.

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 8, 2015

Arrow functions allow for some complex things to happen. It would add unneeded complexity to a javascript engine...

I could be wrong, but my experience with writing compilers tells me the optimization would not be difficult; you would only need the extra logic when you see a pipe operator, and the transformation is entirely based on the AST. For example:

run('task') |> result => process(result, true)

This could be optimized to:

process( run('task'), true )

with zero consequences. The reason a "simple" arrow function is optimizable is because the body is a single expression; in any other case, the optimization would simply be skipped.

@barneycarroll
Copy link

Furthermore, arrow functions are 'unbindable': they have no this and cannot have this applied to them — any reference to this inside the immediate scope of an arrow will look up to the scope in which it is defined. I think it would be truer to say that arrow functions are much simpler than their traditional counterparts (and more 'optimisable' as a result).

@tabatkins
Copy link
Collaborator

Jumping in without reading most of the thread, sorry:

I rather like the placeholder syntax, as it seems useful when decoupled from pipeline as an extremely lightweight arrow function. That is, foo(1, #, 3) just desugars to x=>foo(1, x, 3). That said, it's a savings of three characters, so it probably won't end up being worth it.

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 9, 2015

Ok, so my takeaway from this thread is that the placeholder syntax is not only unnecessary, but also a different issue (partial application). Elixir style is interesting, but I think in the long run piping to the end is more useful than piping to the beginning.

I've aggregated all these ideas into a new version of the pipeline operator. Let's move our discussion to the new issue: #20

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests