Skip to content

Use a promise chain instead? #56

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

Closed
vitaly-t opened this issue Sep 5, 2017 · 19 comments
Closed

Use a promise chain instead? #56

vitaly-t opened this issue Sep 5, 2017 · 19 comments

Comments

@vitaly-t
Copy link

vitaly-t commented Sep 5, 2017

You can simply return a promise from each of such functions, and then write the following:

let result = await doubleSay('hello').then(capitalize).then(exclaim);

or:

let result = doubleSay('hello').then(capitalize).then(exclaim).catch(ops);
// + resolution with an error handler

This creates even better code, IMO, because:

  • syntax .catch for the error handler is nicer/shorter than try{}catch(e){}
  • the chain logic is while just as obvious and short, it is already a standard, with regards to the callback function passed into each .then call.
  • This approach is compatible with ES6 Generators
  • Now this is a big one: No extra effort for asynchronous code, as you get it by default.

I think the existing ES7 is already verbose enough, and not lacking anything, except ahem, better support for classes, closer to C++.


As a bonus, you can simplify it even further, by implementing a function that takes a random value + a list of functions to do .then for each of them.

There are already implementations like this in some promise libraries, so you can do:

let result = await Promise.chain('hello', [doubleSay, capitalize, exclaim]);

This also lets you mix together synchronous and asynchronous code, which is priceless.

@ljharb
Copy link
Member

ljharb commented Sep 5, 2017

This wouldn't be able to be synchronous.

@vitaly-t
Copy link
Author

vitaly-t commented Sep 5, 2017

This wouldn't be able to be synchronous.

await makes it synchronous.

@ljharb
Copy link
Member

ljharb commented Sep 5, 2017

No, it does not. It makes it appear synchronous, but it is not.

@vitaly-t
Copy link
Author

vitaly-t commented Sep 5, 2017

When writing synchronous code this makes no difference. If you think otherwise, then please give an example.

P.S. The best value that my approach brings is to be able to mix synchronous and asynchronous code without extra effort, which is one of the most important features in the modern JavaScript.

@ljharb
Copy link
Member

ljharb commented Sep 5, 2017

It absolutely makes a difference. Polyfills need to happen synchronously, for one - operations that happen on DOM NodeLists would need to be synchronous to ensure that nothing else was mutating the DOM out from under it, etc.

Note that I'm not talking about the code in the line you're awaiting - it's that because await is async, other code in other parts of the app can execute in the meantime. Sync code would guarantee no other code anywhere is running until the line is completed.

@vitaly-t
Copy link
Author

vitaly-t commented Sep 5, 2017

One typically doesn't use asynchronous functions when modifying DOM. But this is just one specific task, not worth consideration in the context of extending JavaScript syntax.

Also, you can use synchronous chain like this:

let result = chain('hello', [doubleSay, capitalize, exclaim]);

with chain function being trivial to implement:

function chain(value, list) {
    list.forEach(a => {
        value = a(value);
    });
    return value;
}

@mAAdhaTTah
Copy link
Collaborator

FWIW, is there support for pipelining a chain of async functions? Or mixing sync & async? e.g.

data |> formatForRequest
  |> await makeRequest
  |> formatResponse
  |> appendResults

@ljharb
Copy link
Member

ljharb commented Sep 5, 2017

The same applies to any mutation, not just the DOM - it's absolutely a core language use case.

Your chain example doesn't preserve the receiver; using .bind there would not be ergonomic.

A synchronous operator is necessary here. If promises were sufficient, this proposal would indeed not need to exist - but they are not.

@littledan
Copy link
Member

@mAAdhaTTah Unfortunately this will not do what you hope it would do. await will tightly bind to makeRequest, and the result will be the same as appendResults(formatResponse((await makeRequest)(formatForRequest(data)))). If makeRequest is a function rather than a Promise, then the behavior will be roughly the same as if you left out the await--it will not be waiting on the request, but instead on the function value itself (which is already there).

@mAAdhaTTah
Copy link
Collaborator

@littledan Is it worth amending the proposal to include mixed sync/async pipelining? I don't know if the syntax suggested above has complexities I'm not seeing, but the current pipeline doesn't easily support async without manually juggling the promises.

Perhaps this also could / should come in a future proposal, a la async iterators.

@mAAdhaTTah
Copy link
Collaborator

My bad, that's duplicated by #53.

@rsxdalv
Copy link

rsxdalv commented Sep 25, 2017

It doesn't even have to be that complicated, @ljharb. @vitaly-t proposal works in only a handful of cases, whereas the pipe operator would make readable many more. What seems to be left out of this conversation is how deep you go into function composition and what side effects you have. Combine these two, and obviously you can't use a promise chain to compose functions that together make up a single transaction, so you are essentially creating more problems, if you then want to introduce locks and whatnot on what should simply be synchronous code. Furthermore, performance isn't going to be great if you use a promise chain on functions like const eq = x => y => x === y; and nevermind the awfulness that will happen to stack traces (if you'll even be able to decipher them)!

Or error handling, since you can accidentally catch what you shouldn't have, thus losing the information for a potentially hard to reproduce problem!

On top of that, any chain function, such as Ramda's pipe, suffers heavily from being so ad hoc that static checking becomes either difficult, or incomplete. As well as any language service you might want to use.

@ljharb
Copy link
Member

ljharb commented Sep 25, 2017

@rsxdalv I believe "doesn't allow for synchronous" is indeed a less complicated way to summarize your complicated comment :-)

@rsxdalv
Copy link

rsxdalv commented Sep 25, 2017

@ljharb I wanted to bridge the gap between "you can't do sync" and "you want to do sync because of DOM manipulation" to more generic cases that everyone will run into. In a way, I think that there still might be some that hit this thread, who might end up shooting themselves in the foot by the proposal of this issue. Though yes, by complicated I meant "specific". I might edit my original comment.

@DylanRJohnston
Copy link

DylanRJohnston commented Sep 28, 2017

@mAAdhaTTah

data |> formatForRequest
  |> await makeRequest
  |> formatResponse
  |> appendResults

desugars into

appendResults(formatResponse(await makeRequest(formatForRequest(data))))

which works so I would assume yes.

@DylanRJohnston
Copy link

DylanRJohnston commented Sep 28, 2017

@vitaly-t await doesn't make it synchronous, it creates a new promise where the result of the promise is bound to the new variable.

async function bar() {
  return Promise.resolve(3)
}

async function baz() {
  return Promise.resolve(4)
}

async function foo() {
  const bar = await bar()
  const baz = await baz()
  return bar + baz
}

Can be viewed as

function bar() {
  return Promise.resolve(3)
}

function baz() {
  return Promise.resolve(4)
}

function foo() {
  return bar().then(bar => {
    baz().then(baz => {
      return bar + baz
    })
  })
}

It's the same way <- gets desugared in Haskell's do notation.

All async functions actually return promises, asynchronous-ness is infections, any piece of code that wants the result of an async function needs to be itself async. You can't work on the result of a computation that doesn't exist yet!

@nmn
Copy link

nmn commented Oct 10, 2017

Just a quick heads up. If await can't work as in the example above, a simple utility function to handle promises can be added instead.

const isThenable = val => val
  && typeof val === 'object'
  && typeof val.then === 'function';

const then = fn => arg =>
  isThenable(arg)
  ? arg.then(fn)
  : fn(arg);

// data |> formatForRequest
//   |> await makeRequest
//   |> formatResponse
//   |> appendResults

data
  |> formatForRequest
  |> makeRequest
  |> then(formatResponse)
  |> then(appendResults)

@littledan
Copy link
Member

The then function is an interesting suggestion. Seems like the original suggestion of the Promise then method subsuming this proposal is not really realistic, so closing this thread.

@itaditya
Copy link

itaditya commented Oct 31, 2017

We can also use simple chaining for synchronous tasks by returning the value from each function, just like jquery does it.

@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
None yet
Projects
None yet
Development

No branches or pull requests

8 participants