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

Proposal: Back to F-sharp #95

Closed
gilbert opened this issue Feb 9, 2018 · 17 comments
Closed

Proposal: Back to F-sharp #95

gilbert opened this issue Feb 9, 2018 · 17 comments

Comments

@gilbert
Copy link
Collaborator

gilbert commented Feb 9, 2018

Inspired by this comment I've pushed up a new proposal: Temporary Bindings. This new proposal seems to be a superior version of the Hack-style proposal, and without using the pipeline operator token.

What this implies is, if this new proposal were to be adopted, then the "pipelining arbitrary expressions" use case would be covered, leaving room for the pipeline operator to revert back to its original F-sharp behavior.

Thoughts?

@js-choi
Copy link
Collaborator

js-choi commented Feb 9, 2018

Reminds me of Clojure’s prefix as-> form, which has an explicit binding before its transformation steps. I raised a pretty similar idea in #84 (comment) a while back:

anArray |> ($) { // The $ may be any arbitrary identifier
  pickEveryN($, 2),
  $.filter(...),
  shuffle($),
  $.map(...),
  Promise.all($),
  await $,
  $.forEach(console.log)
}

A big disadvantage of this temporary-binding proposal as-is is its coupling to variable binding. It cannot be used as an expression itself: this is particularly troublesome with its inability to be used with return, yield, throw, and similar statements.

I don’t know if the Proposal 4 placeholder bikeshedding is a disadvantage that is worth trading for the disadvantage of this proposal. But perhaps it’s worth adding as a fifth proposal to the wiki.

@rbuckton
Copy link
Collaborator

rbuckton commented Feb 9, 2018

This may not be the right issue to mention this, but I wanted to mention that I've created an issue on the partial application repo to address the $.filter(...) scenario on the partial application side, via ?this:

anArray
  |> pickEveryN(?, 2)
  |> ?this.filter(...)
  |> shuffle(?)
  |> ?this.map(...)
  |> Promise.all(?)
  |> await
  |> ?this.forEach(console.log(?));

@gilbert
Copy link
Collaborator Author

gilbert commented Feb 9, 2018

It cannot be used as an expression itself

I should probably make it more clear by putting it upfront, but the last of the other examples demonstrates how it can be used as an expression.

@js-choi
Copy link
Collaborator

js-choi commented Feb 9, 2018

Ah, yes, sorry; I see it now.

@rbuckton: Cool idea. A prefixed keyword like ?this is an interesting idea; perhaps prefixed keywords are another option for Proposals 2,3,4’s placeholder…

@charmander
Copy link

const($) hasAccessByOrgId =
  groups.map(g => g.org_ids),
  new Set($),
  [...$],
  $.map( async id => {
    let has_access = await Role.hasOrgPermission(id, user.id, perm)
    return { id, has_access }
  }),
  await Promise.all($),
  objectify($, { by: 'id' });

I’d rather see that implemented with do:

const hasAccessByOrgId = do {
  let $ = groups.map(g => g.org_ids);
  $ = new Set($);
  $ = [...$];
  $ = $.map(async id => {
    let has_access = await Role.hasOrgPermission(id, user.id, perm);
    return { id, has_access };
  });
  $ = await Promise.all($);
  objectify($, { by: 'id' })
};

It repeats $, but the syntax feels less out of place. (The least awkward version I can come up with is

const hasAccessByOrgId = do $ {
  groups.map(g => g.org_ids);
  new Set($);
  [...$];
  $.map(async id => {
    let has_access = await Role.hasOrgPermission(id, user.id, perm);
    return { id, has_access };
  });
  await Promise.all($);
  objectify($, { by: 'id' })
};

and it isn’t great.)

@gilbert
Copy link
Collaborator Author

gilbert commented Feb 9, 2018

@charmander If you're going to repeat $ =, then you can do without do:

var $, hasAccessByOrgId = (
  $= groups.map(g => g.org_ids),
  $= new Set($),
  $= [...$],
  $= $.map(async id => {
    let has_access = await Role.hasOrgPermission(id, user.id, perm);
    return { id, has_access };
  }),
  $= await Promise.all($),
  $= objectify($, { by: 'id' })
);

But like @rbuckton mentioned, arbitrary variable assignments are probably difficult for the compiler to optimize.

[Edit: grammar]

@js-choi
Copy link
Collaborator

js-choi commented Feb 9, 2018

A new syntactic form doesn’t have to reuse the same binding. It could internally autogenerate new variable bindings for each step, maintaining monomorphism. Having said that, the biggest problem I have with it is that it mandates a prefix form, rather than it being a compact binary form. But I’ll hold off on further judgement until @mAAdhaTTah and I are able to try implementing some real prototypes. It’d be interesting to try a prototype of this too.

@dallonf
Copy link

dallonf commented Feb 9, 2018

How would this combine with the pipeline operator, assuming that some steps in your pipeline might involve single-arg functions (ideal for the F#-style pipeline) or multi-arg (better for this proposal)? Lemme try rewriting the Proposal 2 example from the wiki...

const($) result = anArray
  , pickEveryN($, 2)
  $.filter(...)
  |> makeQuery
  , await readDB($, config)
  |> extractRemoteUrl
  |> fetch
  , await $
  |> parse
  |> console.log // or , console.log($) if log needs to be bound to console

Hmm, assuming all of this works like I think it does, , essentially just becomes the Hack-style pipeline operator. That's not bad.

@gilbert
Copy link
Collaborator Author

gilbert commented Feb 9, 2018

@dallonf True, it's possible to combine them that way. My intuition is that the const($) form would be the go-to pipelining solution for most JS programmers, leaving the F-sharp operator for quick unary composition and other heavier functional use. For example I might write your example a bit differently based on these guessed preferences:

const($) result = anArray
  , pickEveryN($, 2)
  , $.filter(...) |> makeQuery
  , await readDB($, config) |> extractRemoteUrl
  , await fetch($) |> parse
  , console.log($)

Another benefit to const($) is that it requires no new syntax tokens. This help leave room for ? and other potential tokens to be used for the partial application proposal.

@mAAdhaTTah
Copy link
Collaborator

I think my main concern with this proposal is we end up with two different syntaxes for pipelining. In a hypothetical decision between "F# Pipeline + Temporal Variables" vs. "Hack Pipeline", we might be better off with just "Hack Pipeline"; my intuition is the two syntaxes aren't likely to be used together a lot, although I could very well be wrong about this. If they're intended to be complementary rather than competing for similar use cases, I like this approach, but based on your above comment, if you expect Temporal Variables to be the default, I'm not sure I see the benefit of having both syntaxes.

The other concern / question is more political. The pipeline operator has momentum behind it; are you willing to drop pushing for your preferred pipeline solution to throw your support behind a brand new alternative with no support yet? I could see this ending up with only F# Pipeline landing and Temporal Variables stalling, unless the two proposals are tethered in some way. Is that how we intend to present them?


Just to throw it out there (and this could be terrible) but picking up on @js-choi's suggestion above, maybe something like this would be worth exploring?

path
  |> x => x.trim()
  |> ($) {
    $ ?? throw new TypeError(),
    await fetch(`https://example.org${$}`),
  }
  |> x => x.text()
  |> console.log;

This explicitly introduces the placeholder variable into the pipeline and allows the developer to choose its name.

@js-choi
Copy link
Collaborator

js-choi commented Feb 19, 2018

I had been exploring similar parameterized-block syntaxes while thinking about the Proposals and writing Proposal 4’s spec. They may be useful for Proposal 1 but not for Proposals 2,3,4.

In general: Parameterized blocks are similar in functionality to arrow functions as expressions and do blocks as blocks. They may be a useful idea for the block-parameter proposal.

Proposal 1: The parameterized block is useful for F-sharp Style Only pipes in order to perform function-scoped operations. Its functionality otherwise overlaps with using arrow functions as pipeline bodies.

Proposal 2,3,4: The parameterized block in the example above is redundant with simply flattening the block’s contents into the pipeline thread, because throw and await can/will-be-able-to-be expressions. do blocks as pipeline bodies would address statements that alone could not be expressions: x |> do { if (#) { # + 3 } else { … } }.

@gilbert
Copy link
Collaborator Author

gilbert commented Feb 26, 2018

I think my main concern with this proposal is we end up with two different syntaxes for pipelining.

I think this is a valid concern. Perhaps it's not so bad if we take the differences into deeper consideration:

  • Temporary bindings are best for long-form ad-hoc pipelining, where your process spans across multiple lines
  • |> is strictly about invocation. It could be used for pipelining, and it certainly will in fp-land, but its scope is small, focused, and it shines in its use case.

my intuition is the two syntaxes aren't likely to be used together a lot, although I could very well be wrong about this. If they're intended to be complementary rather than competing for similar use cases, I like this approach

I do think they're complementary. If it turns out they are not used together a lot, but still used separately, I think that means they serve different use cases.

The other concern / question is more political. The pipeline operator has momentum behind it; are you willing to drop pushing for your preferred pipeline solution to throw your support behind a brand new alternative with no support yet?

Yes, I'm ok with that. IMO the presence of the Temp Bindings proposal will serve to absorb shock from questions about async and multi-arg function solutions; it will allow |> to stay focused, as opposed to becoming the one-pipeliner-to-rule-them-all.

With all that said, Proposal 4 with a $_ binding is still my favorite ;)

@mAAdhaTTah
Copy link
Collaborator

Given that I expect F# pipelines to use inline arrow functions, I don't see the differences you suggested to be that great, which means I'm inclined to think the differences in usage is likely to be a result of a difference in style preference than one being better than the other. I think we more fundamentally disagree on the ergonomics of arrow functions in the pipeline, which I think is driving our different preferences, although the placeholders do solve the "ad-hoc operator" problem.

IMO the presence of the Temp Bindings proposal will serve to absorb shock from questions about async and multi-arg function solutions

Is this to suggest, with these two proposals, F# would / should not ship w/ the "inline await" rule, instead absorbing that use case in to Temp Bindings?

@gilbert
Copy link
Collaborator Author

gilbert commented Feb 28, 2018

I think we more fundamentally disagree on the ergonomics of arrow functions in the pipeline

Yes, this is probably it. Personally I don't mind typing parenthesized arrow functions with |>, but I also recognize not everyone is as nimble on the keyboard as I am. I think it would be a burden to ask most JS programmers to type the paren-arrow-fn version of the temporary binding examples and anytime they need something beyond a unary function.

Is this to suggest, with these two proposals, F# would / should not ship w/ the "inline await" rule, instead absorbing that use case in to Temp Bindings?

Not would or should, but can. Just throwing it out there as a suggestion.

@mAAdhaTTah
Copy link
Collaborator

I'm still hopeful this is a potential solution for parenthesized arrow functions. Would it still be a burden (in your opinion) if they didn't need to be paren'd?

@gilbert
Copy link
Collaborator Author

gilbert commented Feb 28, 2018

I think would help if it doesn't introduce other confusions. For example, one issue the linked comment pointed out is you need to use parens to use |> in an arrow function body:

const foo = x => (x |> bar |> baz) // works
const foo = x => x |> bar |> baz // doesn't work?

But it any case a good solution like that would make things more palatable, even if not as streamline as the comma operator.

@tabatkins
Copy link
Collaborator

Closing this issue, as the proposal has advanced to stage 2 with Hack-style syntax.

@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

7 participants