-
Notifications
You must be signed in to change notification settings - Fork 164
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
Assimilating thenables-for-promises #75
Comments
It just occurred to me that if we were to try to spec assimilation as: var assimilated = new Promise(function (resolve, reject) {
thenable.then(resolve, reject);
}); it should have the same semantics as var assimilated = anyFulfilledPromise.then(function () {
return thenable;
}); And right now, those don't match, since as I explain above, the current spec implies something more like |
I think that there is only one way to fulfill var fulfilledPromise = makeFulfilledPromiseFor("anyvalue");
var nextPromise = fulfilledPromise.then(function (val) {
// I'd like to pass `objectWithThenMethod` to `nextPromise` (= fulfill `nextPromise` with `objectWithThenMethod`).
// `objectWithThenMethod` has a `then` method, but is not relative to the Promises/A+ spec.
var objectWithThenMethod = {
then: function (arg1, arg2) {
return arg1 + arg2 + 100;
}
};
return {
then: function (f, r) { f(p) }
};
}); (Is that true? There are another ways which I don't know?) Will there be no way to fulfill |
This more fully specifies the manner in which thenables must be assimilated, which is useful both for clarity and for usage in future specifications, e.g. specifying the behavior of `resolve` in a promise-creation spec. The recursive nature of the thenable assimilation is a change from the current specification; see #75 for more details.
This more fully specifies the manner in which thenables must be assimilated, which is useful both for clarity and for usage in future specifications, e.g. specifying the behavior of `resolve` in a promise-creation spec. The recursive nature of the thenable assimilation is a change from the current specification; see #75 for more details.
@nobuoka I think the general consensus is that we really should treat all thenables as promises, and try to assimilate them. If you want to fulfill with a thenable, you need to wrap it, e.g. Basically, our current state is a weird halfway situation, wherein we try to assimilate thenables, but don't do so fully in the case described. We should either go all the way, as in #76, or abandon assimilation of non-promise thenables altogether. The latter seems worse because it reduces interoperability between implementations. |
This more fully specifies the manner in which thenables must be assimilated, which is useful both for clarity and for usage in future specifications, e.g. specifying the behavior of `resolve` in a promise-creation spec. The recursive nature of the thenable assimilation is a change from the current specification; see #75 for more details.
I think that's actually not desirable. A promise represents a value, and that is by definition any JavaScript value including promises and thenables. So if someone fulfills a promise (or, let it be a thenable) with a promise I would expect my outer promise to represent that inner promise, not the value represented by it. In Haskell monad terms, our So I'd suggest to remove the recursive 2.1.2. step from the |
This isn't quite correct. A promise represents a value; it cannot represent a value representing a value---that just means representing a value. |
It happens for both known promises and thenables. Known promises already have a fulfillment value created by flattening any "representation chains." |
Yeah, sure, that's why
Do they? Only those created by |
@domenic: I get your rationale, and I don't necessarily disagree with you, but a promise is a value. That's the whole point of them: they reify the entry points from asynchronous stimuli so you can dynamically compose reactionary processes.
@bergus: In a perfect world, I'd love to have a pure Promise implementation. I love drawing design insight from Haskell. The problem is, Javascript isn't Haskell, so what works well (and beautifully) there can sometimes be much more irritating here. Plus, as @domenic says (I think), this behavior is actually consistent with the core spec. When you call It isn't pure, but it is relatively intuitive. I haven't had any difficulties with this particular aspect myself. |
@bergus promises in first place are aid for asynchronicity, we're always after final resolved values. Resolving promise with unresolved promise so it becomes its resolved value, doesn't make any practical sense, it will just bring headache to users of such promise implementation. When thinking out such problems it's best to test them on real use cases, not just theory, as that may lead you to not practical solutions. |
@medikoo: It can make a lot of sense to me. If it brings you a headache, you won't need to use it :-) A real use case might look like this:
|
That use case is much better served by code like the following though: getInputsFromUserInteraction(...)
.then(function (inputs) {
tell('Thanks for your input"');
return executeTransaction(inputs);
})
.then(function (res) {
tell("Successfully uploaded to "+res);
}, function (err) {
tell("transaction did fail due to "+err);
}); It makes the separation of two promises completely clear. It also closely parallels the synchronous code: var inputs = getInputsFromUserInteraction(...);
tell('Thanks for your input"');
try {
var res = executeTransaction(inputs);
tell("Successfully uploaded to "+res);
} catch (ex) {
tell("transaction did fail due to "+err);
} One of the key goals of promises is to make it easy to translate synchronous code into asynchronous code. It's especially useful in a world where ES6 is just around the corner and soon the async version could look like: var inputs = yield getInputsFromUserInteraction(...);
tell('Thanks for your input"');
try {
var res = yield executeTransaction(inputs);
tell("Successfully uploaded to "+res);
} catch (ex) {
tell("transaction did fail due to "+err);
} |
No, that's not exactly the same - your code tells the user that the transaction failed when he only aborted the input. You would need a switch statement in the error handler whether
|
@bergus let me provide some real world examples of how actually promises are used, when working with async IO: Typical MongoDB setup, used by many, simplified for brevity: // db will hold promise that resolves when connection is open
var db = DB(conf);
// db.collection returns promise, that resolves with access to given collection
var users = db.then(function (db) { return db.collection('users'); });
// users.find also returns a promise
var loggedInUser = users.then(function (users) { return users.find({ email: someEmail }) }); .. or let's e.g. lint all js files in directory: // readdir, promiseLib.map, readFile return promises
var report = readdir(dirname, function (filenames) {
return promiseLib.map(files, function (filename) {
return readFile(filename).then(function (fileContent) {
return lint(fileContent);
});
});
}); As you see, doing this with promise implementation that will treat returned promises as a final values, will need a lot of additional work to get to the real resolved values. Technically such implementation will no longer be a promise implementation as it will no longer help with asynchronous resolutions, and that's the real purpose of promises. If you forgot about that, then you actually not talking about promise implementation, but about some other monad lib, which purpose is uncertain. |
@medikoo: In your second example, you're mixing the Functor and Monad features of a Promise, and it's hard to tell what's happening. (If you saw my post before this edit, you may have noticed my confusion!) Your use of My attempt at making the code more clear: var filenames$ = readdir(dirname, id); // hey, it's claimed to return a promise after all
var report$ = filenames$.chain(function(filenames) {
var lintPromises = filenames.map(function(filename) {
return readFile(filename).map(lint);
});
return sequence(lintPromises);
});
In your first example, you do want the monadic behavior, so you're correct: you would end up with a single level of Promise. But sometimes you want to make the distinction between multiple future threads of computation, particularly in the case of error handling. Given a Promise (Promise a), either promise could fail, and with different errors. You may want different handling behavior for these errors. As it stands, the implicit flattening caused by I grant that in terms of the success path, |
@Twisol I have problems understanding you. What you mean by should have only one level of Promise? Can you also speak with an example, that will in your opinion present this flow a better way?
In
@Twisol I'm not sure what you want to contradict, the way promises work in current implementations doesn't stop you from any error handling you can imagine, and my example is not against that. I just focused here on case of resolving promise with a promise, put error handling aside for brevity as it's not what we're discussing at the moment.
You take that wrong, You are free to do error handling in whatever place you feel it should be done, you're not restricted in that, it's up to you to decide, whether you want to handle that error individually or not. |
@medikoo: I edited my post before I saw you had responded. My response to the second example is now pretty much totally different, and there's now an example of how I would write the code given the behaviors that I seek. Sorry about the confusion!
Yes, but you lose the flexibility of passing the unflattened promise somewhere else, and letting that location handle the errors separately. You're forced to handle it within that single callback, before it gets flattened. |
@Twisol we're talking about one function that invokes complex async operation and that returns one promise. Would you really prefer that instead, such function returns all promises that were involved in obtaining final result? I bet you don't :) Imagine working with such: lintDirectory(path).then(function (promiseA) {
promiseA.then(function (promiseB) {
promiseB.then(function (promiseC) {
promiseC.then(function (report) {
// Finally! process report
});
});
});
}); Not to mention that there may be many not sequential but parallel async jobs involved, and you need to know exactly how deep the result is. Also your example won't work (not to mention confusing details like two different |
@medikoo: Ah, but they are the same. 😉 They are both
Okay. Then // :: String -> Promise Report
function lintFile(filename) {
return readFile(filename).map(lint);
}
// :: String -> Promise [Promise Report]
function lintDirectory(dirname) {
var filenames$ = readdir(dirname);
return filenames$.map(function(filenames) {
return filenames.map(lintFile);
});
} And of course, if you want to boil it down to a single // :: Promise [Report]
var reports$ = lintDirectory(".").chain(sequence); It took only one extra step to conflate the information we no longer care about. // :: Promise (Promise [Report])
var reports$$ = lintDirectory(".").map(sequence); I mean, the code above is really similar to your original code. I'm just explicitly calling out the semantics using different names for each operation. When |
@Twisol if you replaced native
Returned promise with faill with ENOENT error
Returned promise will fail with first error that occurs. Technically Additionally you can make If you need more fine grain customization, you should go level below, iterate files on your own, and address those you want with
Will Simply speaking, you propose to limit functionality of Maybe than better path would be to leave When doing real work you'll nearly never use There are certainly some functional improvements that can be done on Promise/A+ spec (taking inspiration from functional languages), but forcing promises to resolve with a promise as a final value, is a big step back, and just sign that we forgot why we used promises in first place. |
No. And we are totally fine with a What we object against is that multiple-level promise are supposed to be impossible/unusable, and a There are some use cases, and we should not prevent them. Let's assume
Then
When you have a nested |
I did no such thing! The two functions are semantically the same - they fill the same interface. No more, no less.
Why? I customized it just fine, and it works for more use-cases while introducing marginally more complexity. And you're suggesting an options object instead? 😩
It shouldn't, though it wouldn't be an insurmountable issue if it did. If you have an example where the nesting naturally gets deeper than two Promises, please share!
That seems to be the plan. We do want Promises to provide a monadic interface; if you like your
Quite - that would be |
@bergus Your example totally doesn't make sense. You mess with internals which normally are internal logic of generic function, and you don't need and don't have access to. We're after functions that take us from A to B, we're no interested in how it's achieved it internally. Try to write described above |
They're not. In JavaScript filenames.map(function(filename) {
return readFile(filename).map(lint);
}); For any JavaScript programmer this code means: Iterate over a list of files, and for each file iterate over a list of characters in its file content.
You've actually decided not to hide complex logic into a function, but instead expose all internal logic, that's not what makes you productive, and that's not what actually functions are about.
I have examples when it goes well beyond ten, and your asking for more than two ;-) Just do some real async IO projects and you'll see yourself. Examples we put here is kindergarden. Take a look into my projects, in many I work with promises e.g. real world case of linter written with promises: https://github.com/medikoo/xlint/tree/master/lib |
Then there is nothing I can say to sway you, because that's the whole point of these generalized functions. |
@medikoo |
@pufuwozu I know it is in functional languages. I just explained how |
@medikoo I think you're confused about Also |
@pufuwozu yes I know, but some promise implementation, implement readdir(path).map(function (filename) {
return readFile(filename);
});
It's common to use Array generics on various array-likes in JavaScript, and mapping them to other values is a common use case (especially with promises that resolve to lists). that's why it's a convienent to have |
@medikoo none of that makes sense. If Promise were a Functor, only this would make sense: Thanks. |
@pufuwozu It makes big sense to JavaScript programmers. People want to write async code in very close way as they write sync one. Example I've given although presents async flow, looks exactly as sync code in JavaScript. |
@pufuwozu: He's saying it's a map of a map: |
@medikoo create ArrayPromise and it makes sense. As a JavaScript developer, it does not make one bit of sense on a normal Promise. It also breaks the Functor laws. |
@pufuwozu it all depends. In real applications I work on, I want to have visible promise layer as minimal, and declare code nearly as it would be synchronous, above approach works perfectly, it's a real time saver. You actually see real value in a promise object, you see it as a real resolved value, that should be exposed, have specific characteristics, that's very different thinking, and I'm not sure whether it fits what promises/futures are about in first place. |
@medikoo I use Promises as values in many languages. I also abstract away whether my code is executing asynchronously or synchronously. It's great for the real world. You should try it. |
See promises-aplus/promises-tests#20, wherein we essentially have this situation:
I think the reading of the current spec is that
value === fulfilledPromise
. As I said over there:Is this OK? Are there sane wording changes to the current spec that would allow us to have
value === 5
, which is probably more desirable? (Or should we just respectthenableForFulfilled
's wishes?)What do current implementations do? Apparently WinJS.Promise does
value === 5
.The text was updated successfully, but these errors were encountered: