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

Consider removing async function (was promise) integration #3

Closed
JAForbes opened this issue Oct 3, 2017 · 48 comments
Closed

Consider removing async function (was promise) integration #3

JAForbes opened this issue Oct 3, 2017 · 48 comments

Comments

@JAForbes
Copy link

JAForbes commented Oct 3, 2017

EDIT by proposer (TheNavigateur): I'm creating a shortcut to my current conclusion to explain the current closed status of this issue: #3 (comment) and an Explanation of the decision here in the proposal itself

Hi! Thank you for starting this proposal, composition means a lot to me and many others in the community. I hope this feature lands in Javascript, but I hope it lands right. Our language community has a tendency to add additional features to simple specs in the interest of convenience that in the long run actually become inconvenient because they are complex, harder to explain, and affect future proposals which tend to have backwards compatibility with them.

We see composition everywhere, having a built in operator will enable new patterns, it makes me very excited. But we often compose without knowing we're composing, and that can lead to unnecessary friction with library and pattern interop.

A few examples of composition hidden in plain sight in Javascript

  • styled components are just composition
  • decorators, are just composition
  • jsx itself is composition of functions hidden behind syntax
  • Promise chaining, is sort of composition
  • method chaining generally, can be considered informal composition
  • Array::map for example is definitely composition

We compose but we do it informally, and that informality leads to incompatibilities between interfaces without justification. It leads to "special casing".

If we keep compose as simple as possible, hopefully we can encourage the community to rally around that simplicity and receive emergent compatibility between libraries and specifications that were iterated on in complete isolation to eachother.


Composition is beautiful. It's extremely simple but enables so much power in that simplicity.

Promises are the exact opposite. The API is riddled with compromises, and these compromises continue to affect future proposals. One example being, Error handling behaviour of Observables being determined by Promises due to backwards compatibility.

I think there's some alternatives paths we could take to make interop with Promises more seamless without affecting the general simplicity of composition. (I'll write some code examples below) .

Alternatives:

  1. In the future, after this operator lands, have a compose spec that data types can support and this operator will defer to.
  2. Introduce a separate operator that is effectively f => x => x.then(f)
  3. Take no language level action, because there's plenty of elegant userland solutions.

In the future, after this operator lands, have a compose spec that data types can support and this operator will defer to.

Data types can compose. That's what Promise.then sort of approximates after all. For example Array's can compose:

[1].map(f).map(g)

Observables can compose

stream$.map(f).map(g)

Iterators can compose (despite not having language level support yet)

function map * (f, it){
  for( let x of it ){
    yield f(x)
  }
}

// a lazy infinite sequence of squares that can be further composed
const squaredInfiniteRange = map( square, infiniteRange)

Composition is a general principle. Encoding specific support for Promises leads to a highly specific interpretation of composition that we'll constantly be appending to as we add new data types.

But what if we introduce a composition specification, similar to the iterator spec?

Promise.prototype[Symbol.compose] = function(f){
  return this.then(f)
}

Even though then isn't strictly composition, this solution at least allows other data types both in the language and in the community to interop with this new operator without language level coupling.

One confusing aspect of this proposal is, despite composition being general, we should not attempt to automatically compose different types together. E.g. Array's and Promises don't automatically compose with eachother, and they shouldn't. But this compose spec would make it trivial to make a mistake like that.

This problem is usually solved via transformers, but that is a finer point that specification proposal could tackle in it's own context.

Introduce a separate operator that is effectively f => x => x.then(f)

So if we want to compose promises, we can. We just need a util that can lift a function into the promises domain. then allows us to do that.

const then = f => x => x.then(f)
const f = a => Promise.resolve(a + 1)
const g = a => Promise.resolve(a + 2)
const h = a => Promise.resolve(a + 3)

f +> then(g) +> then(h)

If people find they want to compose async values with functions, this solves for that. If it becomes popular, maybe a static then function, or an operator that desugars to that function could be introduced into the language, leaving composition itself simple.

e.g. let's imagine there was an operator :x: that desguared to our then function (defined earlier)

f +> :g: +> :h:

As you can see, we can solve this problem using composition of specifications. Instead of bundling features into one big proposal. That's what composition itself teaches us to do.

Take no language level action, because there's plenty of elegant userland solutions.

I believe, if this proposal only supports functions, that's all we'll ever need. But I've hopefully demonstrated there's plenty of room for expanding on this spec in future versions if we find that isn't the case. I'd really like to give this specification some room to breathe, to be used in real projects and to encourage new practices to flourish in the community. I think this proposal could completely change how we think about and write Javascript, so there's a lot of reason to get this proposal right.

The previous solution is completely definable in userland. It's like adding ingredients when cooking, we can always add to the mix but we can't take away (so be especially careful when adding).


If there's one thing I want JS to get right, its composition. Its so powerful, so generic, so simple.

I implore you, and the community - for this language level formalization, let's keep it as simple as possible.

Thank you for your time.

@evilsoft
Copy link

evilsoft commented Oct 3, 2017

I also agree that Promises should NOT compose like functions compose.
While they can be composed, as @JAForbes mentions, the way they compose is very different and would require Kleisli composition (around the then syntax) composing functions of the form a -> Promise b e. Which of course does not really need to return a Promise due to how the .then chain works.

So while the weakness (as some might say) of not requiring the form a -> Promise b e would seem like they are the same functions through a typical pipe, they are in fact very different. It is my opinion to not muddy the waters even more then we have with how we interact with Promises.

Now that said, I can see the argument of well that is the way they work now, lets just roll with it. But I say that platform is wrought with nothing but hard to maintain flows that will make it hard to refactor from Eager Promises to more the more Lazy, Declarative Types, when the need arises.

Just my 2 cents on the subject.

EDIT:
Oh and 👍 to the separate operator.
Could be easily expanded on to do binds on Promises, Iterators , Streams and Arrays.
Anything that can effectively implement the chain spec in fantasy-land.

@davidchambers
Copy link

Promises are complex, unprincipled, and easy to misuse. Every language feature which treats promises specially is complex and unprincipled as a result.

I would like those developing future ECMAScript releases to stop adding special cases for promises. If one day these people come to agree with me that promises (as specified) were a mistake, I very much hope promises will not yet be irreversibly entwined in numerous language features.

@achung89
Copy link

achung89 commented Oct 3, 2017

Would not go as far as to say that promises are a mistake, but I agree that adding complexity to the operator without proper syntax cues could lead to confusion.

On solution is just to allow for use with await

let pipe = await p1 +> fn1+> await p2 +> fn2

That would make it so that it is scoped only to async functions, which limits the complexity

@keithamus
Copy link

Promises are a useful primitive that have the minimal set to allow them to be swallowed up by await syntax. Thinking of them beyond the scope of await could be a mistake.

But even regardless of Promises, I think this proposal - strategically - should wait to see what becomes of the pipeline proposal; I'm sure people will argue semantics but they are one in the same.

If this proposal bides its time, waits for the pipeline operator proposal to solve these issues (of which there are discussions around), then come in off the back of that, it seems to me (admittedly inexperienced) that this will have far more traction if it aligns to the pipeline proposal. The corollary of this is that the effort in this discussion should probably move there - tc39/proposal-pipeline-operator#31, tc39/proposal-pipeline-operator#53, tc39/proposal-pipeline-operator#56 are all open discussions around this.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 3, 2017

The original intent was not to allow promises as such, but async functions, which are functions after all.

I have updated the proposal to say so instead. Hopefully this satisfies everybody, or have I missed the point?

async functions are now in the language, so I would suggest that supporting async functions in function composition is actually necessary for any such proposal.

Understand that none of the proposals in these comments seem to have fully addressed the following use case, where an asynchronous function is required as the result of a composition:

const getDistanceInLocalUnitsAsync = getDistanceInStandardUnitsAsync +> localizeDistance
//
const distanceInLocalUnits = await getDistanceInLocalUnitsAsync(myRoute);

@JAForbes Will your :f: proposal evaluate to an asynchronous function, or a synchronous one? It needs to be an asynchronous one... so if it does, can you talk me through in more detail how this would be set up, for the given use case above?

@achung89 await f() is used to evaluate the result of the asynchronous function, so await f would seem like a questionable use of the await keyword for this...

@achung89
Copy link

achung89 commented Oct 3, 2017

sorry I misunderstood, I thought the operators was to compose promises not async functions.

You are right, await is a run time evaluation, it might add to the confusion to use that in composition

Even for async fns, I think we should have a different syntax for the same reasons, to make it easier to distinguish between a resolution of a promise vs. the resolution of a fn. (and to reduce ambiguity between a composition that runs synchronously and a composition that runs asynchronously) but that might be just my opinion

@joshburgess
Copy link

@keithamus Composition (compose operator) and application (apply aka pipe operator) aren't one in the same. Personally, if I could have only one, I'd rather have the compose operator... but I'd like to see both in the language.

I'd also like to see both right-to-left and left-to-right variants of each.

@skatcat31
Copy link

skatcat31 commented Oct 3, 2017

Take no language level action, because there's plenty of elegant userland solutions - James Forbes

@JAForbes I'm adding that to my wall of quotes. I like this on a deep personal level.

As for the chosen operator, I have a problem with it being very noisy with arrow functions:

const missyElliot = reverseBits +> v => -1 * v +> v => v--;

// versus

const missyElliot = compose( reverseBits, v => -1 * v, v => v--);

That first one is noisy, confusing, and hard to make out with lambdas, and leads to more typing the more functions you introduce( assuming single character names: line length == # of functions * 3 versus line length == # of functions * 2 + 9). The operator version also leads to confusion of order of operations. Am I doing the left first, than the right like every other use of feed > prior art?

I'm all for leaving this in user land or just a function. Heck it's pretty easy to do on a single line too

// Right to left
const compose = (...fns) => (...values) => fns.reverse().reduce( (v,fn,i) => i === 0 ? fn(...v) : fn(v), values )

// Left to Right, sometimes called pipe or pipeThrough
const compose = (...fns) => (...values) => fns.reduce( (v,fn,i) => i === 0 ? fn(...v) : fn(v), values )

@JAForbes
Copy link
Author

JAForbes commented Oct 4, 2017

@TheNavigateur Ah I think I didn't explain this clearly enough, the above proposals do cover that case, in fact that's what I was assuming you meant originally.

But I imagine you're thinking I'm suggesting wrapping async functions in some decorator or syntax, but I'm instead suggesting to decorate subsequent functions in the composition after an async function is introduced.

That's a mouthful so here's some working code: https://repl.it/MCsF/1

I'll also explain here in case that link ever dies.

If you've got 2 functions, f and g, and f is async you'd compose them like so:

f +> :g: 

Or in vanilla JS:

const then = (fn) => (promise) => promise.then(fn)

compose(then(g), f)
// or
pipe( f, then(g) )

Just like, if a function returned an array, we might want to map over it in subsequent functions:

const f = () => [1,2,3]

const g = x => x * x

pipe( f, map(g) ) //=> [1,4,9]
// f +> :g:

The symbol.compose interface would allow different types to determine what +> does on a particular type, so both proposals would handle the case you are concerned about - because +> would just defer to the compose method on the type which would do as above.

async functions are now in the language,

async functions are in the language. But what does that mean? What is an async function from the perspective of composition?

All async functions are from the perspective of composition, is a function that returns a Promise.

At first it seems simpler to special case async functions. But what about generator functions? What about functions that return any type under the sun, maybe a function that returns an observable will need a special case too?

And there's a lot more types that the JS community isn't considering yet, but one day they will be interested in them and we'll want to add them to the language and special case them as well.

Composing functions that return promises should be no different to composing functions that return arrays. I know that probably sounds ridiculous when you first hear that, but its absolutely true. There's nothing special about async functions from the perspective of composition.

I absolutely understand the sentiment, I used to think much in line with you. But composition itself tells us to build up complex functionality by combining smaller simpler component parts. Having a compose operator that automatically handles promises, observables, generators and so on - goes against the very spirit of composition.


One more aside.

That :x: syntax need not be promise specific either. What were doing there is a more general principle as well, we're lifting a function into a particular context (in this case Promises).

I'm not proposing we add such syntax. But by not special casing particular types we leave the door open for Javascript to organically grow to support new and powerful ideas, by special casing we effectively weld Javascript to programming patterns of the past eternally. That's a general principle that applies to all language proposals. The proposals themselves should be composeable.

If any of these examples still aren't making sense, I apologise, I really do understand what your goals are and they are admirable, I hope I can convince you that we can meet all those goals while also keeping compose super simple.

If you want to go through it 1 on 1, I think my gitter url would be... https://gitter.im/JAForbes

If this was previously making no sense and I convinced you, please consider spreading this idea to other proposals that suggest we need to special case particular data types.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 4, 2017

Promises are gone from the proposal. It was just a logical mistake on my part in relation to async functions. Unless someone objects to async functions being supported as described in the proposal (where the expression evaluates to an async function if one or more of the operands is an async function), I will mark this issue as closed (resolved), leaving about 2 weeks (approx) for any such objections to be initiated, upon which I will either keep this open or recommend moving to a new issue. Sorry for the mistake in the original proposal talking about Promises - I really had meant async functions all along. I had written "Promise (asynchronous function)" thinking that promises are the generic type for async functions, and so supporting them would automatically support async functions plus other cases of the generic type where required, but of course this is wrong - they are not in the same family. An async function returns a promise, it is not itself a promise.

@JAForbes
Copy link
Author

JAForbes commented Oct 4, 2017

FTR I am objecting to async functions. Everything I've written so far has been about async functions. Kind of surprised that hasn't been understood.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 4, 2017

@JAForbes Then could you make a new issue? Promise and async function are different types not hierarchically related, since promises are not functions, and async functions are necessarily functions, and are not themselves promises (but that just happen to return a promise when executed). I've made a significant change on the basis of this issue (removed my illogical use of Promise in the context of my intention to allow async functions as a composition operand). Removing support for async functions themselves, in the way proposed, would be a significant other step.

What error would you show if an async function is used as an operand? "Async functions are not supported in function composition"?

I would defend the support for async functions in a function composition chain on the basis of simplifying async programming with composition, while not clashing in any way with ordinary synchronous composition, but rather complementing it.

@JAForbes
Copy link
Author

JAForbes commented Oct 4, 2017

I've made a significant change on the basis of this issue

Thank you for doing that, its appreciated.

What error would you show if an async function is used as an operand? "Async functions are not supported in function composition"?

You wouldn't throw an error at all - that's valid composition. I think I've shown already that you can compose async functions just fine without actually special casing them in the compose operator. You just need to lift subsequent functions in the composition to the promise context (using the then function I demoed), in the same way you'd need to do traverse any other data type (like the map example I gave for arrays).

Passing a promise into a function is absolutely fine, and in many cases we wouldn't want to automatically build a promise chain for the caller. Its better they are explicit. And it's better the compose operator doesn't treat different data in different ways, it's better for it to not infer intent.

That kind of api design has a place, but at the very edges of a system. Doing it at the language level is limiting. It harms composition.

I would defend the support for async functions in a function composition chain on the basis of simplifying async programming with composition

Are you familiar with Rich Hickey's distinction between simple and easy? Really good stuff, it had a huge impact on the JS FP community, it informs almost every library and interface decision in the community. If not, I recommend checking it out, it may explain the opposition to promises in this proposal - without that perspective it may seem illogical.

I'll paraphrase: Having the composition operator be promise aware is additional functionality and semantics which is by definition more complex. It may seem easier but it's not simpler. It increases the dimensions of the possibility space. It increases the API surface area. And composition, generally, relies on the exact opposite of that.

And out of all the specs where we should pay close attention to the simple vs easy distinction: is composition. Composition is all about extremely simple units that are simple to combine with other simple units. It allows us to build complex functionality by piecing together small simple parts.

Making any one part "convenient" is a local optimization that seems better in the near term but it complicates that unit, it makes it hard to compose, it introduces edge cases. Whenever we do make these concessions, it has to be justified, in this case it isn't.

There's actually a separate type of composition that @evilsoft covered earlier, that is far closer to what you are proposing, it's called Kleisli composition. But - we can build Kleisli composition on top of normal function composition. We can't do that, if function composition in JS has special case behaviour for Promises.

And I'm definitely not saying we can't compose async functions, I'm saying we ignore the fact they are async. Ad we rely on a separate function or operation to build that promise chain for us (if that's what we want to do).

That's what my counter examples were demonstrating. E.g. check out the repl.it link from earlier, I'm composing async functions there, but compose itself has no awareness that the functions are async. It's simpler than that, all it does is connect inputs to outputs, and that's very powerful.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 4, 2017

Can you create a new issue for this? I'm closing this issue in relation to Promises integration as it is not in the proposal.

If you could include your alternative solution to composing asyncFunction1 +> syncFunction1 +> asyncFunction2 +> syncFunction2 into an async function that passes the return value from each into the next (not the promise, the actual return value in the case of the async functions), it would be great.

@JAForbes
Copy link
Author

JAForbes commented Oct 4, 2017

asyncFunction +> then( syncFunction1 ) +> then( asyncFunction2 ) +> then( syncFunction2 )

Or with the syntax I suggested earlier:

asyncFunction +> :syncFunction1: +> :asyncFunction2: +> :syncFunction2:

In the very first post I demonstrated this:

Recall:

const then = f => x => x.then(f)

const f = a => Promise.resolve(a + 1)
const g = a => Promise.resolve(a + 2)
const h = a => Promise.resolve(a + 3)

f +> then(g) +> then(h)

The above are all async functions. I've always been referring to async functions. Moving to a new issue doesn't make much sense to me, I'd only be referring back to things in this thread. The topic hasn't changed at all. And I know better than anyone because I started the thread.

I'm saying this with absolutely no prejudice, but I have a strong impression you haven't been reading what I've been writing, at least not very carefully. And that's a shame because we all care about composition and I believe what I'm proposing has substantial value. I've also put a lot of time and energy into attempting to clarify things, but I'm not seeing any reciprocation.

I'd like to continue this discussion but without any signal that this is a discussion, I'm going to stop posting on this proposal repo.

I feel our energy would be better spent iterating on @isiahmeadows proposal over here: https://github.com/isiahmeadows/function-composition-proposal

I'm glad though to have had this opportunity to make the case for simpler composition. I hope it has made an impact on anyone who read it. But I don't think its wise for me to invest any more time or energy into this particular proposal.

Thank you to everyone who contributed and voted. It's very much appreciated.

@achung89
Copy link

achung89 commented Oct 4, 2017

@JAForbes I think the confusion here is that the title of this issue is "promise integration" and the first post includes primarily promises. The suggestion to make another issue to target specifically async fn seems reasonable as it narrows down the scope of the concern.

@davidchambers
Copy link

@JAForbes, #3 (comment) clarified your position. Thanks for posting it.

Having the composition operator [treat asynchronous functions specially] is additional functionality and semantics which is by definition more complex.

Bingo!

Almost 60 years have passed since LISP was specified and we haven't reached consensus on how to manage complexity in our programs. Composition undoubtedly has a role to play: the composition of two functions which each do one thing is simpler than one function which does two things.

Composition can be explained to a toddler: If you tell me how to get from A to B and how to get from B to C, I can tell you how to get from A to C.

A “fancy” composition operator would not greatly simplify our programs as the operator would itself be a source of complexity. Furthermore, a composition operator which makes choices based on the types of its operands will lead to ambiguities. For the sake of brevity, I'll demonstrate this with Haskell lists.

> (map sqrt . reverse) [1,4,9]
[3.0,2.0,1.0]

It has been suggested that the use of map (or then or whatever the mapping function is named) is inconvenient, and that the solution is to permit f . g as shorthand for map f . g. This would save a few keystrokes, and would be unambiguous in the expression above since sqrt . reverse is invalid.

In some cases, though, both f . g and map f . g are valid (and useful) expressions:

> (reverse . words) "foo bar baz"
["baz","bar","foo"]

> (map reverse . words) "foo bar baz"
["oof","rab","zab"]

Let's keep things simple. Only with simple, principled foundations do we have a chance of creating reliable, maintainable software. ;)

@TheNavigateur
Copy link
Owner

@JAForbes Sorry for my ignorance, how does this pass the actual returned value, and not the promise, to subsequent functions?

Your example is quite abstract so it's really throwing me off understanding it.

Let's take a simple "real" looking example, and you can show me how your solution solves it:

async function getTemperatureKelvinFromServerAsync(place){
    const temperatureKelvin = await server.get('temperatureKelvin', { place });
    return temperatureKelvin; //This is the return value I want to pass to subsequent functions
}

function convertTemperatureKelvinToLocalUnits(temperatureKelvin){
    let temperatureInLocalUnits;
    //synchronously convert kelvin to fahrenheit or celsius based on locale
    return temperatureInLocalUnits;
}

OK, so now I want to compose these 2 functions into a function to which I can pass a "place" and get the temperature in my local units. The composed function is necessarily async, because one or more of the functions, in this casegetTemperatureKelvinFromServerAsync, is async.

With the proposal it's as follows:

const getTemperatureFromServerInLocalUnits = getTemperatureKelvinFromServerAsync +> convertTemperatureKelvinToLocalUnits

Then, I use it anywhere as follows, e.g.:

const temperatureOfLondonInMyLocalUnits = await getTemperatureFromServerInLocalUnits(london);

//

const temperatureOfNewYorkInMyLocalUnits = await getTemperatorFromServerInLocalUnits(newYork);

How do you accomplish this specific functionality with your approach?

I'm not trying to catch you out, I'm just not yet seeing the exact parallel

@davidchambers
Copy link

@TheNavigateur, one would need something like this:

const getTemperatureFromServerInLocalUnits =
  compose(then(convertTemperatureKelvinToLocalUnits),
          getTemperatureKelvinFromServerAsync);

then (fmap in Haskell) “lifts” a function to the world of lifted types. In this case it takes a function of type Kelvin -> LocalUnits and returns a function of type Promise Kelvin -> Promise LocalUnits.

@TheNavigateur
Copy link
Owner

@davidchambers Can you explain how it forces the composed function getTemperatureFromServerInLocalUnits to be an async function?

@ScottFreeCode
Copy link

If I may have a go at this (at risk of overexplaining)... I find it's often helpful, when dealing with async functions or arrow functions, to compare with the equivalent ES5 code (even if said equivalent is Not Pretty). In the first of these two examples wrapper is what will actually be composed, in the latter two examples promise => ... is returned from syncFunction => ... and is what will actually be composed):

// async/await
function then(syncFunction) {
  return async function wrapper(asyncInput) {
    const pseudoSyncInput = await asyncInput
    return syncFunction(pseudoSyncInput)
  }
}

// ES5
function then(syncFunction) {
  return function wrapper(promise) {
    return promise.then(syncFunction)
  }
}

// arrow lambda
const then = syncFunction => promise => promise.then(syncFunction)

// spoonful of alpabet soup: functional-programming's assembly code and breakfast of champions
const then = f => x => x.then(f)

Any of these freestanding then implementations should work for the "compose with freestanding then" examples above since the behavior for all of them should be:

  • take a synchronous function
  • return an async/promise-returning function (effectively turning the synchronous function into this one)
  • which takes a promise (thus enabling ordinary composition with other functions that return promises)
  • and when actually called applies the synchronous function to the input promise's value (with then or with await)
  • returning a promise containing the result of the synchronous function (either from then or from the async function implicitly wrapping the return value in a promise)

@TheNavigateur
Copy link
Owner

@ScottFreeCode Sorry, but this doesn't seem to solve the problem. The problem is to get a reusable asynchronous function composed, that can take, in this example, a "place" (not a promise) as an argument, such that you can do:

//where "getTemperatureFromServerInLocalUnits" is the composed async function

const temperatureOfLondonInMyLocalUnits = await getTemperatureFromServerInLocalUnits(london);

//

const temperatureOfNewYorkInMyLocalUnits = await getTemperatorFromServerInLocalUnits(newYork);

Have I misunderstood?

@ScottFreeCode
Copy link

ScottFreeCode commented Oct 6, 2017

...Well, now I'm fairly confident I'm going to be repeating some things that have already been discussed, so I apologize if any of the following is already clear or obvious, but to put my explanation in context: then doesn't replace the compose operator. then does the promise-unwrapping (some would say the conversion of a non-promise-taking function into a promise-taking function) so that the compose operator doesn't have to. In more detail:

The functions in the example problem have the following signatures.

  • getTemperatureKelvinFromServerAsync takes "place" and returns promise containing Kelvin
  • convertTemperatureKelvinToLocalUnits takes Kelvin and returns other units
  • then(convertTemperatureKelvinToLocalUnits) takes promise containing Kelvin and returns promise containing other units, by the process described in the then definition
  • getTemperatureFromServerInLocalUnits needs to take "place" and return promise containing other units

The types "promise containing Kelvin" as the output of the first function and "Kelvin" (no promise) as the input to the second don't match up; so, without then, you'd have to make the compose operator "unwrap" the Kelvin from the promise to pass it to convertTemperatureKelvinToLocalUnits as in your example:

const getTemperatureFromServerInLocalUnits = getTemperatureKelvinFromServerAsync +> convertTemperatureKelvinToLocalUnits

But! then (as implemented in depth above) can handle the promise-unwrapping outside of the compose operator; then(convertTemperatureKelvinToLocalUnits) takes a "promise containing Kelvin" as its input and so the types do match up and can be used with a compose operator that doesn't do anything special with promises:

const getTemperatureFromServerInLocalUnits = getTemperatureKelvinFromServerAsync +> then(convertTemperatureKelvinToLocalUnits)

@davidchambers
Copy link

davidchambers commented Oct 6, 2017

@TheNavigateur, I suggest watching Railway Oriented Programming to gain intuition for how a function of type Promise a -> Promise b may be derived from a function of type a -> b. I found Scott Wlaschin's way of explaining the concept quite natural.

@dead-claudia
Copy link

@TheNavigateur You might also want to take a look at my proposal, specifically this section which suggests a potential way to allow lifted async function composition without conflating sync vs async.

@TheNavigateur TheNavigateur changed the title Consider removing promise integration Consider removing async function (was promise) integration Oct 7, 2017
@TheNavigateur TheNavigateur reopened this Oct 7, 2017
@skatcat31
Copy link

Why the title change?

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 7, 2017

@JAForbes @ScottFreeCode @davidchambers OK I get it now, at least partly.

Instead of allowing

sync1 +> sync2 +> async1 +> sync3 +> sync4 +> async2 +> sync5

as proposed, you are proposing we would require for all functions (whether sync or async) following an async function

sync1 +> sync2 +> async1 +> :sync3: +> :sync4: +> :async2: +> :sync5:

where :: in :myFunction: converts myFunction to

async promiseFromLastFunction => myFunction(await promiseFromLastFunction)

Am I right?

Wouldn't this fall over upon a subsequent :asyncFunction:, though?:

async promiseFromLastFunction => myAsyncFunction(await promiseFromLastFunction)

Doesn't this now return a promise of a promise (instead of promise of the required value), thereby messing up the chain?

Your clarification / thoughts?

@TheNavigateur
Copy link
Owner

@skatcat31 Because at the time the issue was raised I had mistakenly stated in the proposal that promises should be allowed as an operand, which I subsequently removed. The title change is to clarify that the objection is to integration of async functions as described in the proposal.

@dead-claudia
Copy link

@TheNavigateur

sync1 +> sync2 +> async1 +> :sync3: +> :sync4: +> :async2: +> :sync5:

What you're proposing is equivalent to this:

sync1 +> sync2 +> async1
  +> (async x => sync3(await x))
  +> (async x => sync4(await x))
  +> (async x => async2(await x))
  +> (async x => sync5(await x))

This would result in a chain equivalent to this:

async function foo(...args) {
    let r1 = async1(sync2(sync1(...args)))
    let r2 = sync3(await r1)
    let r3 = sync4(await r2)
    let r4 = async2(await r3)
    let r5 = sync5(await r4)
    return r5
}

The thing is, the lifted variant doesn't really seem much different than the unlifted one, which is bound to confuse people. Compare these two:

sync1 +> sync2 +> async1 +> :sync3: +> :sync4: +> :async2: +> :sync5:
sync1 +> sync2 +> async1 +> :sync3: +> sync4 +> :async2: +> :sync5:

Do you notice the difference? If it takes you longer than a few seconds, it's too subtle.

Couple other questions/thoughts:

  • Would :foo: become a valid expression elsewhere, like in doSomething(:foo:)? It'd be kind of an otherwise pointless feature that I doubt would get past TC39.
  • Consider the fact you could already lift any function now with const then = f => async x => f(await x). Unless it becomes a commonly used feature, please let it wait until the need actually arises.
    • Async functions appeared because co had already taken off within a matter of months
    • Callable class constructors were withdrawn because decorators and functions were sufficient, and it mostly died as people got used to ES6 classes.

@dead-claudia
Copy link

dead-claudia commented Oct 7, 2017

@TheNavigateur

[Me:] Unless it becomes a commonly used feature, please let it wait until the need actually arises.

This is also why I listed async function composition as a potential expansion, rather than including it by default in my proposal.

@ScottFreeCode
Copy link

ScottFreeCode commented Oct 7, 2017

@TheNavigateur

Wouldn't this fall over upon a subsequent :asyncFunction: , though?:

async promiseFromLastFunction => myAsyncFunction(await promiseFromLastFunction)

Doesn't this now return a promise of a promise (instead of promise of the required value), thereby messing up the chain?

The "right"/generic way to handle this is to just leave asyncFunctions as-is, ie instead of:

sync1 +> sync2 +> async1 +> :sync3: +> :sync4: +> :async2: +> :sync5:

...do:

sync1 +> sync2 +> async1 +> :sync3: +> :sync4: +> async2 +> :sync5:

However, to answer the question directly: The language doesn't let you create a promise of a promise. It really gets in the way of that generic code thing mentioned upthread where promises, arrays, other objects we haven't even invented yet can all share a common "do stuff with the contained value" and "compose with this object" interface, but promises (at least as the language currently specifies them) automatically unwrap any promise you feed into them to prevent getting a promise of a promise. It's weird, because, just as in this proposal, it's not that hard to write a small (and generic!) helper function to handle that unwrapping specifically, and then not special-case anything implicitly. But as it is you wouldn't have to worry about somebody accidentally writing :asyncFunction: in practice, because it wouldn't change anything in effect.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 8, 2017

@ScottFreeCode Can you point me to the documentation that specifies that a promise of a promise is impossible in ES? Just curious

The "right"/generic way to handle this is to just leave asyncFunctions as-is, ie instead of:

sync1 +> sync2 +> async1 +> :sync3: +> :sync4: +> :async2: +> :sync5:

...do:

sync1 +> sync2 +> async1 +> :sync3: +> :sync4: +> async2 +> :sync5:

I am not sure this is correct. Surely the subsequent async functions would need to accept the promise returned by the previous function. As-is, they accept the raw value as specified in their params list. Am I right?

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 8, 2017

@isiahmeadows I'm not proposing it as such: it was proposed at the start of this thread. I'm just examining it - and I largely agree with what you've said. My main concern is that :asyncFunction: / then(asyncFunction) logically returns a promise of a promise, not a promise of the required value, for the next function. I may try it out to confirm one way or the other, when I get time

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 8, 2017

OK I can confirm that awaiting a promise of a promise automatically awaits its resultant promise recursively, until the first non-promise value: https://jsfiddle.net/pnz68199/4/ (this uses setTimeouts to illustrate the recursive behavior at each stage). Seems illogical, but that's the way they've done it

@ScottFreeCode
Copy link

I am not sure this is correct. Surely the subsequent async functions would need to accept the promise returned by the previous function. As-is, they accept the raw value as specified in their params list. Am I right?

Aha -- well, it depends, yes:

  • For "async functions" that take a promise and return a promise, no helper function is necessary.
  • For "async functions" that must take a value and return a promise, what we need is another helper function that turns a function taking a raw value and returning a promise into a function taking a promise and returning a promise. This other helper function might avoid wrapping the returned promise in another promise in the first place if possible, or might let it be nested but then flatten the nesting (if used properly it should make no difference which to my knowledge).

Interesting tidbit: In generic terms rather than promise-specific ones, the function that applies a regular function (one both taking and returning values of some other type) to a special type's contents is called map (or fmap in some places), while one that works instead for a function that takes input of some other type but returns output of the special type is called flatMap or chain (it enables the kind of chaining we're discussing!) or, if I'm not mistaken, bind. These types of generic functions can typically be implemented for just about any type imaginable, from the humble array to parser systems and quantum effect simulators (or, for something in between, the failure-handling described in that Railway-Oriented Programming tutorial linked by another commenter).

The issue we've run into is that the only way JavaScript gives us to implement map or flatMap/chain for Promises, the promise's .then function or Promise.resolve, both behave as both map and flatMap/chain, choosing the one or the other behavior depending on whether the function it's "lifting" returns (or, for resolve, the value it's wrapping is) a promise or not. Convenient if you don't want to have to keep code updated for whether values or promises are being passed around, not so convenient if you want to fit promises into a generic framework leveraging map and flatMap/chain without needing to special-case any particular types. Hence the desire to try to minimize the spread of this kind of promise special-casing in new features. The other commenters here can probably tell you more about that though -- some of them have built such frameworks, I'm just trying to learn them.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 9, 2017

@JAForbes I'd like to address a couple of points you made, and maybe you can re-join this discussion.

Composing functions that return promises should be no different to composing functions that return arrays

I agree with this, and that's not exactly what this proposal does. A synchronous function that returns a promise would be treated like any other synchronous function. The proposal only recognises functions that are explicitly declared as async functions as signalling the intention to pipe their declared return value (e.g. return 5) to the next function when completed, and thus also produce an async function as a result. I know that async functions return promises, but they are are a different object type (AsyncFunction, not Function), are declared differently, especially in their declared return value (e.g. return 5). I think this is important.

what about generator functions?

Exactly how you would intuitively expect it to be composed:

generatorFunction +> ordinaryFunction

creates a generator function that pipes each yielded value to ordinaryFunction as an iterator.

generatorFunction +> asyncFunction

creates an async generator function that can be used as an async iterator.

What about functions that return any type under the sun, maybe a function that returns an observable will need a special case too?

They too would evaluate to what you would intuitively expect them to.

I know this might go against what you said, but I think this way adds a lot more expressive power when doing async and/or generator programming than having to create and do a lot of repetitive "lifting" (that requires understanding that promises recursively await until a non-promise value, as mentioned recently) etc.

I am not sure I understand your idea of allowing other types (other than functions) to use the +> operator, or is that not what you were suggesting? If not, then what would be a concrete example to demonstrate that allowing the piping of the declared return or yield values in explicitly declared async and generator functions respectively would be a bad idea (i.e. what would it block, for example)? If, on the other hand, you are for allowing non-functions to use the +> operator, I'm not sure I think that would be a good thing - that would allow some very convoluted code to be developed. Besides which, it would allow AsyncFunction and GeneratorFunction to possibly be used to allow what I've proposed anyway. I've proposed it as strictly functions-only, and intuitively based on the function domain you are introducing. I think this is the best for code readability, bug avoidance and expressive power. How would you do generatorFunction +> ordinaryFunction without auto-piping the yielded values in generatorFunction, for example?

@ScottFreeCode
Copy link

A synchronous function that returns a promise would be treated like any other synchronous function. The proposal only recognises functions that are explicitly declared as async functions...

Just to be clear: you're proposing that the consumer of a library, if he wishes to compose with the library's exported functions, must distinguish between whether the library exports "synchronous promise-returning functions" or "functions that also synchronously return promises but were written with a particular keyword syntax that dubs them asynchronous"? That is, not only the returned types but the way the function was written by its author needs to be kept in mind by the users of a function and the author ought to communicate it to them as part of the API, and both of them need a concept of "synchronous promise functions" that differs from some functions that synchronously return promises?

Because that would be vastly more confusing than promises unwrapping other promises. There are generic code problems caused by the conflation of "do this to the promise's value and wrap the result in another promise" and "do this to the promise's value and return this promise as the result", but the fact that promises can avoid nesting is hardly obscure: it's one of their basic advantages over callbacks (not knowing about it is listed as "Rookie Mistake 1" in a promise tutorial that's so useful I've given it to people who were struggling to understand -- wait for it -- async functions). In contrast, I can barely even state, nevermind explain, the proposed difference between async functions and functions that return promises; I certainly don't want to have to document it as part of my APIs or to have to figure out which way the functions were written in a given version of a given library that doesn't happen to document it in order to be able to use the new composition/piping operator.

I'd object even if I'd never heard of the generic coding issues with special-casing types.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 9, 2017

async functions are of type AsyncFunction. They are different. Functions that return promises are just of type Function.

async functions are in many ways less confusing than promises. They abstract away the need to even think about promises:

async function get5(){
    return 5;
}

async function getSomething(){
    const something = await get5(); //5
}

Using async and await skips over the need to even understand what promises are. Kids can start consuming and using async functions well before they understand how to create a promise.

Yes, documentation should clarify that a function is async and not just a synchronous function that returns a promise. That's because it's a different object type, and should be understood as such. Even if the author doesn't communicate this, it is trivial to check this at run time via instanceof.

@ScottFreeCode
Copy link

ScottFreeCode commented Oct 9, 2017

Right now I can pass those async functions to any code expecting these promise functions and vice versa because they behave identically:

function get5(){
    return Promise.resolve(5);
}

function getSomething(){
    get5().then(function(something) {
    })
}

Ideally that should not cease being true; certainly it should not cease being true despite the fact that in and of themselves they still behave identically. The fact that you don't want to think about the promises underneath the async hood should not force me to start thinking about some type-based difference between these behaviorally identical functions! If I want to have to care about the nominal types instead of the behaviors I'll go back to Java where I have to write boilerplate wrapper code to fit together objects that do the same thing but were not declared to implement the same type.

@ScottFreeCode
Copy link

Also, although it's less directly relevant: I regularly help people troubleshoot their async function usage in terms of the equivalent promise code. async is cleaner but doesn't eliminate all the "gotchas" of promises and when you run into those "gotchas" the easiest way to understand the issue can be understanding promise behavior; I know this from experience.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 9, 2017

The fact that they behave identically in the context you describe does not mean that they are the same thing or must always be treated the same. For one, they are literally a different runtime object type so I wouldn't say there is "equivalent promise code" so much as "analogous" - since there is nothing stopping features being added to AsyncFunction and/or Function in the future that cause them to diverge.

Secondly, the way async functions are declared is important.

Semantically, I am describing an async function as returning 5 when I say return 5. When I use and declare async functions as someone that may or may not be interested in the details of promises, I am interested only that I get 5 when the function result is awaited. In function composition, therefore, I am expecting that the value "5" is piped to the next function in the chain, not a promise.

You can continue to use synchronous functions that return promises if you want. And you can even use all of your "then" constructs etc. if you are more comfortable to do so. I am not sure why you would, given the simplicity offered by async. I am not sure what "gotchas" you are talking about, so perhaps you could give an example.

I wonder, do you oppose that AsyncFunction was introduced?

@TiddoLangerak
Copy link

TiddoLangerak commented Oct 10, 2017

In the repository for async functions it has been explicitly discouraged to make a distinction between "normal" functions that return a promise, and async functions. To quote @domenic:

Please do not do this. Any function that returns a promise is an async function.

(ref).

I think it would be a really bad idea to suddenly start treating them differently, and I highly doubt if you'll be able to get that past the tc.

@ScottFreeCode
Copy link

Hi @TheNavigateur,

The internet ate my reply. 😭 I am out of stamina to recreate it in full, sorry, but the gist goes like this:

I'd like to apologize if my previous comments were less than constructive! I admit I lost my cool more than I'd like, based on the purely irrational criteria that the idea got "closer to home" for me.

Sticking to just the two most relevant examples, here's the surprising anti-nesting behavior of promises interacting with the way async functions output and await promises:

async function returnsPromise() {
  return Promise.resolve(42)
}

async function usesPromise() {
  const promise = Promise.resolve(42)
  console.log(promise, typeof promise, typeof promise.then)
}

usesPromise()

And the second example -- well, it's more of a short story, and touches on async function usage and their returned promises but really ties back to the main point of this issue. I help support an old library with an API that takes a callback, and (for reasons that are fairly obvious to users of the library) if the callback returns a promise then the API waits for that promise. We've found it quite convenient that async functions Just Work as callbacks for this API since they return promises, but once in a long while a user will be experimenting trying to solve some other problem and will (in a regular callback) call an async function without returning its output. The API, since the promise isn't returned to it, presumes that the callback was synchronous -- and, since it threw no errors, successful. It cannot tell the user about this mistake, since it cannot tell apart the mistake from choosing a different valid behavior because it branches off of a particular type (I don't know that it's using instanceof, but the idea -- and the resulting brittleness in some cases -- is similar). And as long as it's used correctly there's no problem, but the problem occurs when things don't meet a very particular idea of what usage is "correct", which we're now locked into. If it were two separate functions for synchronous and asynchronous usage, it would be easy for each to guarantee correct behavior or useful errors in all cases; but since it's a single function that implicitly varies behavior based on what happened to be output, things got a bit messier. Easy enough to correct (whether, as I tend to, by pointing out the dropped promise, or whether ignoring the promise and just focusing on the fact that the async function isn't, broadly speaking, composed with anything), but the API could be doing the correction automatically instead of misbehaving.

(Wow, I rewrote that as quick as I could and it still came out long and rambly. I guess it's true it takes time to write a short one. Or at least to write an organized one split up into smaller pieces. Well, I apologize for rambling too.)

I don't necessarily think there'd be a problem with changing the usage and/or behavior of async functions, if anyone can propose a way to do so that "fixes" some of the dangers such as the one I just described (where an unused promise output goes and runs off on its own and is ignored and -- until Node terminates on unhandled rejections -- suppresses any exceptions it throws too). What I'm... well, I don't want to say "concerned" as though justifying my initial alarm, but let's say "wary of" to tone it down a bit -- what I'm wary of is triggering different behavior in other things based on reading an object's or function's type (howsoever that's done). There are some ways to do that predictably, safely and without loss of flexibility, but it's a pretty high bar to reach.

I think that's most of what I'd wanted to say! If you would like I may be able (depending on how much time I have -- I have other things I've promised, pun not intended, to review and handle this week) to dig up more examples, but I'm hoping to move back toward the original topic -- I feel like I brought up a red herring bringing my experience into it, after all my experience could be flawed and I'd be the last person to know.

@ScottFreeCode
Copy link

Tangentially -- although I don't want to speak for anyone else here -- I doubt it's anyone's goal that we would in the long run need to litter our code with "lifting" boilerplate, especially any full of promise-specific knowledge. In fact, I rather suspect that's the opposite of the end game. I've probably gone on long enough, so I'll try to be really brief here, but --

Imagine a function const map = action => array => array.map(action) -- now you can singleValueFunction +> singleInputMultipleOutput +> map(singleValueFunction) +> arrayInputWhateverOutput, just like using promises and our then helper. Now imagine you realize the array of data needs to be asynchronous -- you could redesign your composed pipeline to use promises of arrays... or, without touching the pipeline, you could just swap out observables for arrays as long as they also implement the method map! (Maybe rename array in that helper to object.) And as previously discussed, map could also be implemented on promises (then without unwrapping a returned promise).

Now you've got an operator and a helper where you can:

  • choose whether to operate on the contained value (map) or on the container (no map -- this option is what can be messed up by an API that automatically unwraps a contained value, by the way)
  • swap out different containers without changing the composition code, to get asynchronicity, multiple values etc.

In other words, choice of helper controls what you do with structure, while choice of type controls what behavior the structure provides, and these can be switched independently of each other and used with any type -- even new ones in the future -- just by implementing the type's map method. How powerful is that? (Now create an operator that's shorthand for "compose with map" the way +> is shorthand for basic function composition, to get rid of the boilerplate altogether. And, since you might want to unnest arrays and probably want to unnest promises, do the same thing we just did for map, only for flatMap -- the previously discussed "map and unnest" method; for promises it's then except it will error instead of becoming map if you make a mistake and return a non-promise, for arrays it's something like concat the output of map, etc.) Arguably it's one of the best APIs possible: the API that unifies other APIs.

(I don't know if getting a map method on most objects' prototypes is exactly how the functional programming community would like to get to there, but I'm fairly sure this is a rough idea of the goal for composition and special types.)

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 10, 2017

Hi @ScottFreeCode , I seem to have a hard time understanding these abstract examples, and how they would apply in "real life" scenarios. Can you give me a concrete example of where allowing the +> operator to pipeline the declared return value in AsyncFunction and produce an AsyncFunction would block anything you might want to do? Let's compare the power of being able to trivially compose AsyncFunctions, GeneratorFunctions and AsyncGeneratorFunctions from other AsyncFunctions, GeneratorFunctions and AsyncGeneratorFunctions vs losing that triviality to be able to do "other things" instead that would not otherwise be possible (what are those things exactly, via concrete, "real life" sounding examples?).

@ScottFreeCode
Copy link

ScottFreeCode commented Oct 10, 2017

For AsyncFunction, how about error handling on the promise? If it's not special, you can do this:

const handleError = handler => promise => promise.catch(handler)
const warnNonCriticalError = handleError(error => { isNotCritical(error) ? console.warn(error) : throw error })
asyncFunction +> warnNonCriticalError +> then(useValue)

Not necessarily the best approach ever, but should +> unwrap asyncFunction's returned promise before it ever has a chance to go into warnNonCriticalError?

Or, if we're extending this behavior to generators, what if you want to do something with the state of the generator instead of (or even before/after) iterating over it, rather than just doing something with the value(s) from it?

More generally (but not necessarily any more abstractly), the more types of data and/or functions that get treated specially, the more likely it is that one of them will return something we might want to use directly instead of automatically unwrapping.

I also want to correct one other thing (and provide a corresponding example) in my previous explanation, having reflected on it a bit more -- it's not only cases where you would want to not-map a returned container/structure value that are affected by automatic unwrapping. Consider this for example:

// presented again for clarity
const map = action => object => object.map(action)

// the meat of the function
getUserPages +> map(renderPage)

// ok!
const getUserPages = () => ["bio", "contact info", "wall", "thing that will replace the wall next year"]

// ok?
const getUserPages = () => getObservableStreamFromRemote("user pages")

I'm assuming for sake of example that observables will have a map function just like arrays. I mean it would be kind of weird if the type that's basically asynchronous arrays didn't have the quintessential array function, right?

And as long as observables are treated the same as arrays -- i.e. treated the same as any other value -- the hypothetical freestanding map helper/lifting function presented above can be used in this code and you don't have to update it when you discover that you need to get the data from an asynchronous source and switch from a local, synchronous array to an observable.

But if, after all the other discussion goes through, we decide to add observables to the data types that will be treated as asynchronous function output and somehow unwrapped, what happens? Does the map helper still work because the observable was unwrapped into an array? Or did it get resolved into a single value and the map function no longer works? Or, worst possibility of all, did it get resolved into a single value, but the contents of the array were also supposed to be objects that happen to have a map function, so the helper is now mapping over the wrong object entirely?

A bit contrived, perhaps -- presumably if observables are automatically unwrapped, it will be into arrays. But it's not really about observables specifically (switching between arrays and observables was just an easy "realistic" example; another one is switching between an async function and a function that returns a promise, but we've already been down that road...). It's an example of where treating a type specially, not only in general (as in some of my past examples) but in the particular way that's proposed for async functions, would mean that you can't rely on using the same code on special and non-special types, at least not nearly as easily. The apparent triviality of the special behavior goes down quite a bit once one ends up in a situation where even one of those kinds of questions actually has to be asked, and special behavior for a type makes it pretty much impossible to rule out such questions altogether.

Anyway, I'm not sure how much more I can contribute, sorry, and again I apologize if my contributions haven't all been ideal; but I hope I've been at least somewhat helpful as far as I've been able to go.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 10, 2017

@ScottFreeCode

For AsyncFunction, how about error handling on the promise? If it's not special, you can do this:

const handleError = handler => promise => promise.catch(handler)
const warnNonCriticalError = handleError(error => { isNotCritical(error) ? console.warn(error) : throw error })

asyncFunction +> warnNonCriticalError +> then(useValue)

async functions automatically reject when you throw inside them.

Thrown exceptions are automatically caught by a try catch around the consumer await call.

This is clean and standard.

Of course warnings can be shown before the async function's execution in the "async piping version" as well.

So I wouldn't call that an advantage either way.

Or, if we're extending this behavior to generators, what if you want to do something with the state of the generator instead of (or even before/after) iterating over it, rather than just doing something with the value(s) from it?

Then have a function before the generator function, that pipes its return value to it.

More generally (but not necessarily any more abstractly), the more types of data and/or functions that get treated specially, the more likely it is that one of them will return something we might want to use directly instead of automatically unwrapping.

How could you use a promise other than awaiting it? How could you use an iterator other than iterating with it? Even if so, and you wanted to do something "non-composey" with the promise or iterator, I would recommend composing up until that point, executing it, then doing your funny business with it, before continuing, instead of losing all that composition power.

But if, after all the other discussion goes through, we decide to add observables to the data types that will be treated as asynchronous function output and somehow unwrapped, what happens?

The proposal is for function composition, so only functions would be allowed. function, async function, function* and async function* cover all the possibilities. That's all there is. Observables are most easily modelled by async function*. I'd still love to see a real world sounding code example which shows how piping the declared return / yield values of async and generator functions with function composition would actually harm their programming instead of helping it. Abstractly there are operator overloading concerns which are understandable but mixing function types is necessary for generator / async function composition in many cases, for example. e.g.

const random0To100Generator = random0To1Generator +> multiplyBy100

...in this case multiplyBy100 is an ordinary synchronous function, but we needed it to help us compose our new generator. The same applies for processing async function results synchronously and piping them to other async functions. That's why introducing a new operator @> to compose async and e.g. *> to compose generators and @*> to compose async generators would probably leave us in the same place as just using +>. I'm not sure the pattern proposed at the start of this issue (a boilerplate then for every function following an async function) is good or desirable - it requires extra understanding to implement correctly, and hence a greater surface area for bugs / mistakes. This is why I believe the trade-off of overloading the operator is worth it. Just like we generally benefitted from being able to do 'Number value is ' + number + ', boolean value is ' + booleanValue, despite the concern about overloading, except in this case the benefit is more extreme, and the concern seems less understandable in the real problem space.

@TheNavigateur
Copy link
Owner

TheNavigateur commented Oct 13, 2017

Closing the issue, until someone demonstrates any real practical advantage, in any scenario, of piping the underlying promise to the next function and producing a Function instead of piping the declared return value to the next function and producing an AsyncFunction, upon the introduction of an AsyncFunction to the operator. Preferably, via a realistic code example comparing each approach in the exact same scenario.

In particular, address the following concerns:

  1. The declared return myValue of an AsyncFunction leads to the expectation of piping myValue, and not a promise, to the next function in the chain. (Yes, despite the underlying implementation, I'm asking you to appreciate the language semantics of AsyncFunctions). This keeps it consistent with how synchronous functions are seen to pipe their declared return myValue too.

  2. The presence of overloading a standard operator, +, in the case for example of myNumber+'myString' producing a string, has been and continues to be understood and used perfectly intuitively since inception.

  3. The simplicity offered in composing AsyncFunctions from AsyncFunctions and Functions vs the necessary boilerplate and relatively deep understanding of promises required to implement the same with the alternative (e.g. the fact that the proposed then(asyncFunc) recursively unwraps a promise of a promise for async functions upon awaiting the composed promise result) means a smaller surface area for bugs and mistakes in the problem space.

Address these concerns, and I can gladly reopen this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests