Closed domenic closed 11 years ago
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 thenable.then(fulfill, reject)
.
Or should we just respect thenableForFulfilled's wishes?
I think that there is only one way to fulfill nextPromise
with a object which has then
method (it may be not relative to the Promises/A+ spec) as below:
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 nextPromise
with a object which has then
method (it may be not relative to the Promises/A+ spec) if changes to the current spec which are discussed in this issue are made, won't there? No problem?
@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. return { myObj: objectWithThenMethod }
or return [objectWithThenMethod]
.
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.
@domenic That's okay with me. I realize the concept that we should either go all the way as in #76, and I agree.
allow us to have value === 5, which is probably more desirable?
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 then
method already acts as map
and >>=
(bind) combined. I strongly oppose mixing this with join
as well.
Especially, in the 8f31f9b draft this joining procedure only happens for thenables, not for known promises - and promises (from other implementations) that are not recognised as such are "only" thenables, bringing in a little inconsistency.
So I'd suggest to remove the recursive 2.1.2. step from the Assimilation
procedure.
A promise represents a value, and that is by definition any JavaScript value including promises and thenables.
This isn't quite correct. A promise represents a value; it cannot represent a value representing a value---that just means representing a value.
Especially, in the 8f31f9b draft this joining procedure only happens for thenables, not for known promises
It happens for both known promises and thenables. Known promises already have a fulfillment value created by flattening any "representation chains."
A promise represents a value; it cannot represent a value representing a value---that just means representing a value.
Yeah, sure, that's why then
is like a bind
.
Yet, we can have promises that represent a promise (or a special subclass of it?), and I could think of some situations where it would be helpful to pass whole promise objects around.
Known promises already have a fulfillment value created by flattening any "representation chains."
Do they? Only those created by then
, since the actual promise construction is still implementation-dependent (not counting the resolvers-spec).
A promise represents a value; it cannot represent a value representing a value---that just means representing a value.
@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.
In Haskell monad terms, our then method already acts as map and >>= (bind) combined. I strongly oppose mixing this with join as well.
@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 promise.resolve()
with a promise, it flattens the doubled-up Promise down into one thread of computation again.
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:
getTransactionFromUserinteraction(…).then(function(transaction) {
tell("Thanks for your input!");
transaction.then(function(res) {
tell("Successfully uploaded to "+res);
}, function(err) {
tell("transaction did fail due to "+err);
});
}, console.log.bind(console, "userinteraction aborted");
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 err
is an UserinteractionError
or a TransactionsubmitError
- or you would need to nest it like
getInputsFromUserInteraction(…).then(function (inputs) {
var transaction = executeTransaction(inputs);
// What if I wanted to abstract out the tell() feedback *here*
// - move the below code into a callback function?
tell('Thanks for your input');
transaction.then(function (res) {
tell("Successfully uploaded to "+res);
}, function (err) {
tell("transaction did fail due to "+err);
});
}, function(err) {
console.log("userinteraction aborted");
});
@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 then
on readFile(filename)
suggests an fmap
more than a bind
, so the result of your mapping callback is ostensibly a single, non-nested Promise. However, I can't tell what promiseLib.map
does. It's mapping over a list of files, so I would expect it to return [Promise]
, but instead you claim it returns a Promise (of something). Is it a Promise [LintResult]
or a Promise [Promise LintResult]
? If the former, it's clearly doing more than a simple map. I would expect [Promise LintResult]
to be the more obvious result, just from reading the code itself. And then readdir itself returns a promise, and there's no hint as to whether it treats its callback as a map or a monadic action. Probably both depending on duck typing.
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);
});
sequence :: [Promise a] -> Promise [a]
is something you'd already have defined elsewhere, probably in that promiseLib
object. I grant it isn't the best name - I stole it from a similar Haskell function operating on IO instead of Promise - but it's already clear that it does something different. I've also explicitly used map
and chain
instead of the ambiguous then
. I know I wrote this version, so I'm biased, but I think it makes the different behaviors more apparent.
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 then
causes this opportunity to be lost. You would be forced to add error handling within the confines of the outer then
- you couldn't defer the error handling to elsewhere, because once you leave the scope of the then
, you've lost the distinction between the two threads.
I grant that in terms of the success path, Promise a
and Promise (Promise a)
are equally useful: when the computation is a success, you get an a
either way. It's in the failure path where the distinction becomes much more useful. Analogously, when you're doing a lookup two levels deep in a structure, and your result is Maybe (Maybe a)
, if you get a Nothing, you need to know where the lookup failed in order to determine how to proceed. And you don't always want to handle it in the same place as where the failure occurs.
@medikoo: In your second example, you're implicitly using only the Functor part of a Promise. Your end result ?should have only one level of Promise precisely because you aren't invoking any "later" threads of computation - just the one incurred by readdir.
@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?
There is no contradiction here - the function just shouldn't be called then.
In then
both callbacks are optional, and here (as error handling is passed to the invoker) it is used perfectly, mind that readFile
returns promise, we don't get file content immediately.
readFile(path).then(cb)
means: when content of file at path
is obtainted, then pass result to cb
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.
@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.
As it stands, the implicit flattening caused by then causes this opportunity to be lost.
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. Mind that in many flows it is desirable to not handle error on each step, but just do one crash handling at the end. It's actually another advantage that promises have over working with regular callbacks (where you need to repeat verbosely same error handling on each call).
@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!
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.
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.
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 map
methods).
To make it clear: let's base our discussion on lintDirectory
function that returns promise which resolves with array of lint reports for each file in directory.
That's both simple and real world example. Usually flows are more complicated (as other things/options need to be taken care of). If you complicate things at this stage, be prepared for big headache when working on real projects.
Also your example won't work (not to mention confusing details like two different map methods).
@medikoo: Ah, but they are the same. :wink: They are both (a -> b) -> m a -> m b
. One implements m ~ Promise
, the other implements m ~ Array
, but they have precisely the same semantics: apply a function to the a
s in the m
. This is what it means to be a Functor! :grinning: And this is what @pufuwozu is up in arms about: reusing the same semantic operations over any type that supports them.
To make it clear: let's base our discussion on
lintDirectory
function that returns promise which resolves with array of lint reports for each file in directory.
Okay. Then lintDirectory :: String -> Promise [Report]
. This appears to be a sensible return type for the success path, but it loses potentially-valuable information for the failure path. For example, what if the directory doesn't exist? What if one of the files could not be read? Either the errors are conflated, or you drop one of them silently. It's easy to envision a situation where you'd want to handle these, and handle them individually. The ideal type for this circumstance is Promise [Promise Report]
, where the outer Promise is from readdir
and the inner is from readFile
.
// :: 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]
, you can just do:
// :: Promise [Report]
var reports$ = lintDirectory(".").chain(sequence);
It took only one extra step to conflate the information we no longer care about. sequence
swaps the order of the [] and Promise in the type, and chain
flattens out the doubled-up Promise. Or, if you still wanted to hang on to the distinct possibility that the directory wasn't there (i.e. Promise (Promise [Report])
), but are willing to discard the missing files, you could do:
// :: 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 then
is overloaded to do many things (fmap
, bind
, join .: bind
, join . join .: bind
, ...), it becomes really difficult to trace the program's intent.
Ah, but they are the same. They are both (a -> b) -> m a -> m b. One implements m ~ Promise, the other implements m ~ Array,
@Twisol if you replaced native Array#map
with something different, that's even more confusing, no JS programmer will apprieciate that.
If you mean that both are promises with map
function, then you need to know that map
concept in JavaScript is already coined, and have a bit different meaning, users of your library will be a bit confused seeing different map
thing. Pick other name.
Okay. Then lintDirectory :: String -> Promise [Report]. This appears to be a sensible return type for the success path, but it loses potentially-valuable information for the failure path. For example, what if the directory doesn't exist?
Returned promise with faill with ENOENT error
What if one of the files could not be read?
Returned promise will fail with first error that occurs. Technically lintDirectory
can also wait for all eventual errors to fail, and then reject with error that will hold references to all file errors, but it was discussed once, and common sense is to fail as fast as first error occurs.
Additionally you can make lintDirectory
customizable and say via option to ignore eventual single file errors. e.g. lintDirectory(path, { ignoreFileErrors: true })
If you need more fine grain customization, you should go level below, iterate files on your own, and address those you want with lintFile
, which obviously also should be available.
And of course, if you want to boil it down to a single Promise [Report], you can just do:
Will chain
flatten the result not matter how many promises are chained? If not, then mind, it won't work, as it's just simplified example and usually nest is deeper than two calls.
If it will, then I hope you realize that it provides us with same functionality that then
currently does (?)
Simply speaking, you propose to limit functionality of then
so it's 100% monadic and introduce other function that brings it back.
Maybe than better path would be to leave then
as it is (as by introducing chain
you've realized its behavior is useful and needed) and introduce a different function that would do what you expect then
to do now (?)
When doing real work you'll nearly never use then
(the way you see it), and you'll clutter your code with chain
. If you haven't yet configured async IO flow with promises, please do (It would also make our conversation much more constructive). I'm pretty sure that in final call you'll understand that the way then
is provided by popular libraries is most practical and expected way.
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.
@medikoo
As you see, doing [the examples] 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. Would you really prefer that such function returns all promises that were involved in obtaining final result?
No. And we are totally fine with a then
that assimilates the returned promise, i.e. then :: Promise a -> (a -> Promise b) -> Promise b
, that is what bind
is required to do. It allows you to join everything neatly together, that's why monads are so useful :-)
(and personally I'm even fine with overloading it with fmap
behaviour, it makes the method more handy when it automatically lifts non-promise values)
What we object against is that multiple-level promise are supposed to be impossible/unusable, and a then
that does recursively flatten the callback result - only one flattening level should be allowed.
There are some use cases, and we should not prevent them. Let's assume
readFile :: filename -> Promise filecontents
lint :: filecontents -> Promise lintreport
Then readFile("…").then(lint).then(alert, console.log)
is perfectly fine. Yet, another application could be
// p :: Promise (Promise lintreport)
var p = readFile("…").then(function(f) { return Promise.of(lint(f)); }); // == readFile("…").fmap(lint)
p.then(function(lp) {
console.log("file read");
return lp.then(beautifyreport, console.log.bind(console, "report could not be created:"));
}, console.log("filesystem fail:"))
.then(alert.bind(window, "here is your report"), console.log.bind(console, "something failed:"));
When you have a nested p
(not saying that all promises should be) and don't want to inspect it like above, you still could use p.join().then(alert, console.log)
.
@Twisol if you replaced native Array#map with something different, that's even more confusing, no JS programmer will apprieciate that.
I did no such thing! The two functions are semantically the same - they fill the same interface. No more, no less. Promise#map
applies a function to the resolved value of a promise and puts the result back into a promise. Array#map
applies a function to every value in an array and puts the results back into an array. Or: for every value in the object, map
replaces the value with the result of applying a function to that value. Or: map
lifts a function from working on normal values to working on normal values in the Functor.
If you need more fine grain customization, you should go level below, iterate files on your own, and address those you want with lintFile
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? :weary:
Will chain flatten the result not matter how many promises are chained? If not, then mind, it won't work, as it's just simplified example and usually nest is deeper than two calls.
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!
Maybe than better path would be to leave then as it is (as by introducing chain you've realized its behavior is useful and needed) and introduce a different function that would do what you expect then to do now (?)
That seems to be the plan. We do want Promises to provide a monadic interface; if you like your then
helper that's fine too, but we want the monadic and functorial operations to be available.
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.
Quite - that would be map
, not chain
, and chain
is the whole reason Promises are monads. However, I've seen that quite often you do want to use map
, and that's currently broken in the Promise-of-a-Promise case.
@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 lintDirectory
with your idea of then
. Mind that in real world lintDirectory
may invoke tens of async operation to obtain result, but caller is just interested in promise that resolves with lint reports list.
I did no such thing! The two functions are semantically the same - they fill the same interface.
They're not. In JavaScript Array#map
maps values of a list into other list (and list term is generic, doesn't necessarily means array). You wrote:
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.
Adding to that, in some promise implementations map
called on promise is registered as a fallback for Array#map
that is then run on resolved value. So in case of your example it will indeed apply lint to each character of file content.
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?
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.
If you have an example where the nesting naturally gets deeper than two Promises, please share!
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
They're not. In JavaScript Array#map maps values of a list into other list (and list term is generic, doesn't necessarily means array).
Then there is nothing I can say to sway you, because that's the whole point of these generalized functions.
@medikoo map
is a function that applies to any functor. See the Fantasy Land specification. Array is an example functor. Promise can also be a functor (and should be).
@pufuwozu I know it is in functional languages. I just explained how map
works in JavaScript, and how such code would be understood by JS developer. map
run on string will map each character of string individually and return instance of Array. It's how JavaScript is specified.
@medikoo I think you're confused about readFile
then. It's not going to return a String, it's going to return a Promise of a String (I think you'll agree that we wouldn't want to block). The Promise should be a functor which would allow it to be mapped over.
Also String.prototype.map
is undefined. Would have to be called Array.prototype.map.call(s, f)
which works because String is defined to be an ArrayLike.
@pufuwozu yes I know, but some promise implementation, implement map
on promise as a fallback for list.map
which would be called on a list that becomes a resolved value.. thanks to that you can do:
readdir(path).map(function (filename) {
return readFile(filename);
});
readdir(path)
returns promise, and map
on promise actually fallbacks to [filename1, filename2..].map
when promise is resolved.
String.prototype.map
is not implemented. but as you noticed Array.prototype.map
is implemented as generic one (intentionally) so it can be used on any array-like also string.
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 map
fallback directly on promise.
... implement map on promise as a fallback for list.map which would be called on list that becomes a resolved value
@medikoo none of that makes sense. If Promise were a Functor, only this would make sense: readdir(path).map(function(filenames) { /* ... */ })
. Notice "filenames" - the function gets passed an array of filenames.
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: (a -> b) -> (Promise [a] -> Promise [b])
.
@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
.