-
Notifications
You must be signed in to change notification settings - Fork 2
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
Comments
I also agree that Promises should NOT compose like functions compose. So while the weakness (as some might say) of not requiring the form 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: |
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. |
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 |
Promises are a useful primitive that have the minimal set to allow them to be swallowed up by 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. |
The original intent was not to allow promises as such, but I have updated the proposal to say so instead. Hopefully this satisfies everybody, or have I missed the point?
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 @achung89 |
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 |
@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. |
@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: 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 ) |
@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 +> :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
All 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 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. |
Promises are gone from the proposal. It was just a logical mistake on my part in relation to |
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. |
@JAForbes Then could you make a new issue? 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. |
Thank you for doing that, its appreciated.
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 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.
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 |
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 |
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. |
@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. |
@JAForbes, #3 (comment) clarified your position. Thanks for posting it.
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 In some cases, though, both > (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. ;) |
@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 case 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 |
@TheNavigateur, one would need something like this: const getTemperatureFromServerInLocalUnits =
compose(then(convertTemperatureKelvinToLocalUnits),
getTemperatureKelvinFromServerAsync);
|
@davidchambers Can you explain how it forces the composed function |
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 // 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
|
@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? |
...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: The functions in the example problem have the following signatures.
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 const getTemperatureFromServerInLocalUnits = getTemperatureKelvinFromServerAsync +> convertTemperatureKelvinToLocalUnits But! const getTemperatureFromServerInLocalUnits = getTemperatureKelvinFromServerAsync +> then(convertTemperatureKelvinToLocalUnits) |
@TheNavigateur, I suggest watching Railway Oriented Programming to gain intuition for how a function of type |
@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. |
Why the title change? |
@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 async promiseFromLastFunction => myFunction(await promiseFromLastFunction) Am I right? Wouldn't this fall over upon a subsequent 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? |
@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 |
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:
Do you notice the difference? If it takes you longer than a few seconds, it's too subtle. Couple other questions/thoughts:
|
This is also why I listed async function composition as a potential expansion, rather than including it by default in my proposal. |
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. |
@ScottFreeCode Can you point me to the documentation that specifies that a promise of a promise is impossible in ES? Just curious
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? |
@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 |
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 |
Aha -- well, it depends, yes:
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 The issue we've run into is that the only way JavaScript gives us to implement |
@JAForbes I'd like to address a couple of points you made, and maybe you can re-join this discussion.
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
Exactly how you would intuitively expect it to be composed: generatorFunction +> ordinaryFunction creates a generator function that pipes each yielded value to generatorFunction +> asyncFunction creates an async generator function that can be used as an async iterator.
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 |
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 -- I'd object even if I'd never heard of the generic coding issues with special-casing types. |
async function get5(){
return 5;
}
async function getSomething(){
const something = await get5(); //5
} Using Yes, documentation should clarify that a function is |
Right now I can pass those 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 |
Also, although it's less directly relevant: I regularly help people troubleshoot their |
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 Semantically, I am describing an async function as returning 5 when I say 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? |
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:
(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. |
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 (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. |
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 Now you've got an operator and a helper where you can:
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 (I don't know if getting a |
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 |
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 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. |
Thrown exceptions are automatically caught by a 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.
Then have a function before the generator function, that pipes its return value to it.
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.
The proposal is for function composition, so only functions would be allowed. const random0To100Generator = random0To1Generator +> multiplyBy100 ...in this case |
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 In particular, address the following concerns:
Address these concerns, and I can gladly reopen this issue. |
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
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:
f => x => x.then(f)
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:Observables can compose
Iterators can compose (despite not having language level support yet)
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?
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.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 ourthen
function (defined earlier)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.
The text was updated successfully, but these errors were encountered: