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

Assimilating thenables-for-promises #75

Closed
domenic opened this issue Feb 13, 2013 · 35 comments
Closed

Assimilating thenables-for-promises #75

domenic opened this issue Feb 13, 2013 · 35 comments
Milestone

Comments

@domenic
Copy link
Member

domenic commented Feb 13, 2013

See promises-aplus/promises-tests#20, wherein we essentially have this situation:

var fulfilledPromise = makeFulfilledPromiseFor(5);
var thenableForFulfilled = {
  then: function (f, r) {
    f(fulfilledPromise);
  }
};
var assimilatedThenable = makeFulfilledPromiseFor("whatever").then(function () {
  return thenableForFulfilled;
});

// What should happen here?
assimilatedThenable.then(function (value) {
  // should `value === 5` or `value === fulfilledPromise`???
});

I think the reading of the current spec is that value === fulfilledPromise. As I said over there:

When attempting to adopt thenableForFulfilled's state, you can only rely on the then method, which is clearly telling you that it's fulfilled with fulfilledPromise. In particular, it's not telling you thenableForFulfilled is waiting on fulfilledPromise; if that were the case, it wouldn't call you back immediately.


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 respect thenableForFulfilled's wishes?)

What do current implementations do? Apparently WinJS.Promise does value === 5.

@domenic
Copy link
Member Author

domenic commented Feb 15, 2013

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).

@nobuoka
Copy link

nobuoka commented Feb 16, 2013

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?

domenic added a commit that referenced this issue Feb 17, 2013
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.
domenic added a commit that referenced this issue Feb 17, 2013
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.
@domenic
Copy link
Member Author

domenic commented Feb 17, 2013

@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.

@nobuoka
Copy link

nobuoka commented Feb 17, 2013

@domenic That's okay with me. I realize the concept that we should either go all the way as in #76, and I agree.

domenic added a commit that referenced this issue Feb 21, 2013
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.
@bergus
Copy link

bergus commented Feb 25, 2013

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.

@domenic
Copy link
Member Author

domenic commented Feb 25, 2013

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.

@domenic
Copy link
Member Author

domenic commented Feb 25, 2013

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."

@bergus
Copy link

bergus commented Feb 25, 2013

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).

@Twisol
Copy link

Twisol commented Feb 25, 2013

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.

@medikoo
Copy link

medikoo commented Apr 11, 2013

@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.

@bergus
Copy link

bergus commented Apr 14, 2013

@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");

@ForbesLindesay
Copy link
Member

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);
}

@bergus
Copy link

bergus commented Apr 14, 2013

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");
});

@medikoo
Copy link

medikoo commented Apr 15, 2013

@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.

@Twisol
Copy link

Twisol commented Apr 15, 2013

@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
Copy link

medikoo commented Apr 15, 2013

@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).

@Twisol
Copy link

Twisol commented Apr 15, 2013

@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.

@medikoo
Copy link

medikoo commented Apr 15, 2013

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.

@Twisol
Copy link

Twisol commented Apr 15, 2013

Also your example won't work (not to mention confusing details like two different map methods).

@medikoo: Ah, but they are the same. 😉 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 as in the m. This is what it means to be a Functor! 😀 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.

@medikoo
Copy link

medikoo commented Apr 15, 2013

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.

@bergus
Copy link

bergus commented Apr 15, 2013

@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
Copy link

Twisol commented Apr 15, 2013

@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? 😩

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.

@medikoo
Copy link

medikoo commented Apr 15, 2013

@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.

@medikoo
Copy link

medikoo commented Apr 15, 2013

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

@Twisol
Copy link

Twisol commented Apr 15, 2013

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.

@puffnfresh
Copy link

@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).

@medikoo
Copy link

medikoo commented Apr 15, 2013

@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.

@puffnfresh
Copy link

@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.

@medikoo
Copy link

medikoo commented Apr 15, 2013

@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.

@puffnfresh
Copy link

... 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.

@medikoo
Copy link

medikoo commented Apr 15, 2013

@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.

@Twisol
Copy link

Twisol commented Apr 15, 2013

@pufuwozu: He's saying it's a map of a map: (a -> b) -> (Promise [a] -> Promise [b]).

@puffnfresh
Copy link

@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.

@medikoo
Copy link

medikoo commented Apr 15, 2013

@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.

@puffnfresh
Copy link

@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.

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

7 participants