Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Make always() more sane #103

Closed
briancavalier opened this Issue · 56 comments

6 participants

@briancavalier

Right now, promise.always() is weird for a few reasons, several of which were discussed in this google group thread. For instance:

  1. It receives either a fulfilled value, or a rejection reason and can't easily distinguish, except by inspecting the value/reason itself. This makes the function passed to always() trickier to implement and potentially more error prone.
  2. It is easy to accidentally "handle" a rejection simply by returning. For example, passing the identity function or a function that returns undefined, like console.log to always() will always squelch errors.

One idea would be to try to make always() work more like synchronous finally. Consider the following code:

try {
  return doSomething(x);
} catch(e) {
    return handleError(e);
} finally {
    cleanup();
}

Notice that this finally clause does not have access to the return value unless we do some extra work above and capture it in a variable. So, the "default" is for finally NOT to have access to the return value or to the exception. While cleanup can throw an exception, thus transforming an originally-successful return into a failure, it cannot turn a failure into a successful return by not throwing.

Now consider how the current always() implementation works in this code:

var fn = require('when/function');
fn.call(doSomething, x)
  .otherwise(handleError)
  .always(cleanup);

There are two obvious differences:

  1. cleanup has access to the result or the error.
  2. cleanup can modify the outcome of the operation in a way that it can't above: it can transform a failure into success simply by returning (even if it simply returns its input, i.e. the identity function!).

Here is a proposed version of always that I think behaves more like (but still not exactly like) finally:

Promise.prototype.always = function(callback) {
  return this.then(
    function(value) {
      return when(callback(), function() {
        return value;
      });
    },
    function(error) {
      return when(callback(), function() {
        return when.reject(error)
      });
    });
  )
}

Notice that callback is not given access to either the fulfillment value or the rejection reason. It can still turn a success into a failure by throwing, but it cannot turn a failure into a success simply by returning. It also cannot change the ultimate fulfillment value.

I'm becoming a fan of this, but want to see what other folks think.

One potential variant of this would be to pass the value/reason to callback, but still to disallow the case of turning a failure into success. That would look like:

Promise.prototype.always = function(callback) {
  return this.then(
    function(value) {
      return when(callback(value), function() {
        return value;
      });
    },
    function(error) {
      return when(callback(error), function() {
        return when.reject(error)
      });
    });
  )
}

Thoughts? Alternatives?

@unscriptable

After some thought, I really like the idea that "clean up" is the top use case for this functionality. It seems that other use cases are easily served by .then() and .otherwise() -- even if we have to do .then(func, func).

Explicitly passing the same function for onFulfilled and onRejected ( .then(func, func)) makes it obvious that you want the function to be polymorphic.

Now I'm wondering if "always" is the right word. While it doesn't force an end to the chain, it certainly feels like the end of a unit of work when it's framed as an analog to "finally".

doSomething().otherwise(handleError).always(cleanup).then(doSomethingElse);

Heh. When I see it like that, "always" seems like the right word. Nm, I guess.

@unscriptable

+1 for not passing anything to .always()

@Twisol

I'm not sure "always" is a good name for this kind of functionality. It suggests semantics similar to that of "then" and "otherwise", when this is actually more like Ruby's Object#tap than anything. Maybe "tap" or "guarantee" would be better?

@briancavalier

@unscriptable Wow, so much good stuff in your response. tl;dr you've completely convinced me that this new approach is the way to go :)

Explicitly passing the same function for onFulfilled and onRejected ( .then(func, func)) makes it obvious that you want the function to be polymorphic.

You could have stopped here, and I would have been convinced :) The synchronous analog to .then(f, f) is a great way to visualize why this makes sense:

try {
  return f(doSomething(x));
} catch(e) {
    return f(handleError(e));
} finally {
    cleanup();
}

You must add two calls to f(), which exactly parallels .then(f, f). Note that while you could assign the result of doSomething or handle Error to a value and call f inside or after the finally, that's not parallel to returning promise.then().otherwise().always().

Now I'm wondering if "always" is the right word. While it doesn't force an end to the chain, it certainly feels like the end of a unit of work when it's framed as an analog to "finally".

Right, and the thing to remember about finally is that it doesn't have to be the last thing in a function. You can (obviously!) still execute code after the closing brace of a finally. So, we shouldn't think of always as the absolute end of a promise chain, which leads me to:

doSomething().otherwise(handleError).always(cleanup).then(doSomethingElse);
Heh. When I see it like that, "always" seems like the right word. Nm, I guess.

I agree, it seems perfectly reasonable. In this case .then() is simply acting to stitch together the previous doSomething/otherwise/always triad (which is an analog to a try/catch/finally triad!), with code that follows the triad (i.e. code after a finally). So, with this newly proposed .always() behavior, it will be exactly parallel to the synchronous:

var result;
try {
  result = doSomething(x);
} catch(e) {
    result = handleError(e);
} finally {
    cleanup();
}

doSomethingElse(result);

I'm convinced!

@briancavalier

@Twisol you've raised a great point! We have some options:

  1. Change the behavior of always(). Since this breaks backward compat, we must wait for 2.0 (which is not really far off at all)
  2. Introduce a new API for this, and play the name game. We can introduce this anytime we want, e.g. 1.8.1, and then we have sub-options:
    1. Deprecate always() because we feel it is useless or hazardous, and remove it in 2.0
    2. Leave it because we feel it is useful and worth the hazards.

We can have the name discussion after we decide which way we want to go :)

@renato-zannon

Until now I've used always only for convenience - as a shorthand for then(func, func) - and I agree that the error-eating behavior can be a little confusing. So, +1 for a change on that!

About the API, I think that modifying the existing always might be confusing if people are aware of how it works now (and want to use it like that). So, I would vote for 2.1 on @briancavalier's options :)

@briancavalier

@Twisol I just read about ruby's tap(), very interesting! Without getting into the naming discussion just yet, it led me to realize that this new functionality could be implemented as:

Promise.prototype.always = function(callback) {
  var self = this;

  return this.then(injectHandler, injectHandler);

  function injectHandler() {
    return when(callback()).yield(self); // <- whoah!
  });
}

Interestingly, that is likely to be more efficient considering how when.js's internal machinery works.

<3 promise.yield()

@unscriptable
@briancavalier

Ok, I guess we play the name game a bit to see where it leads us :) Some contenders, in no particular order:

tap - Quite a nice word on its own, imho. However coming from a non-ruby perspective, it doesn't seem intuitive to me. I get the feeling from reading that tap article and its comments that it confused rubyists as well.

guarantee - also a nice word; on the longish side (but hey, we also have "otherwise", so who cares). What are we guaranteeing? promise.guarantee(myFunc) ... although, for certain function names, e.g. promise.guarantee(cleanup), it may be quite intuitive.

finally - why not? :) Are there still environments we care about that would have to quote this? Consider this: alias the new when/function's call to try, alias otherwise to catch and implement this new functionality as finally:

var fn = require('when/function');
return fn.try(
    doSomething, x, y
).catch(
    handleError
).finally(
    cleanup
);

Looks surprisingly like its synchronous counterpart. The oddly placed return is the only ugliness, although perhaps a good reminder that this isn't actually synchronous at all.

That raises the question of whether we should try to look so similar to sync code or not. Would it confuse people?

@unscriptable

Looking back at your example for always as tap, it seems that the callback is necessarily a side-effect of the promise pipeline. In that sense, the word "tap" kinda makes sense to me. However, it's more like "also do this other thing and then continue". What is the word for that?

@briancavalier

Ah, right, good point about thinking of it as side-effect. Although, you're right: it's not a completely detached side effect ... we're not spawning a new thread to do something "over there". It's still temporally attached: it can return a promise to delay the next link in the chain.

"do this other thing then continue" thoughts: aside, pause, wait, meanwhile, interrupt, intervene, soliloquy

"meanwhile" is a great word, but since it implies "at the same time", it's not the right meaning here :( We should save it for some other purpose, tho!

@renato-zannon

To add to the pool of options, there's also ensure, which is basically ruby's finally (if we forget about some ruby-only edge cases). I think it has a semantic very similar to guarantee, except for the direct parallel it brings for rubyists (is that desirable?). Using the previous example:

doSomething().otherwise(handleError).ensure(cleanup).then(doSomethingElse);

I like the similarity that finally and the aliases bring! As for the confusion, I think that using the usual formatting it is diminished:

fn.try(doSomething, x, y).catch(handleError).finally(cleanup);

@briancavalier

A thought on "tap": Putting aside what I know about ruby's tap now, to me tap implies a way to "tap into" the promise chain, i.e. to observe the current state, but not change it, a la "wire tap". That's not necessarily what we're doing here, since we aren't granting access to the value/reason.

That said, "wire tap"-like functionality seems like something that could be an interesting addition, although I haven't thought through what it would actually do :)

@briancavalier

"ensure" - good one! Like you said, it's "guarantee" with fewer keystrokes.

@briancavalier

Oh, I know! promise ... hmmm, wait, nm ;)

@briancavalier

Here's some real code to stare at, as well as unit tests. I used the name "finally" here, but only because I needed to pick something to write code. We can still change it based on discussion here.

@Twisol

Hah, I like that implementation. Definitely makes it clear that the callback isn't part of the main flow.

"finally" and "ensure" are excellent candidates. If things were renamed so we have "try" and "catch", that would be nice, although two thoughts spring to mind:

  1. Since try/catch/finally/ purposefully evoke an imperative pattern, make sure they don't interact non-intuitively with other functions, or behave strangely if they're used out-of-order.
  2. Since "call" is in "when/function", you would only have "catch" and "finally" by default unless you included the auxiliary module. This seems very strange!

I personally think "ensure" has less imperative baggage, but I could go either way.

@briancavalier

Since try/catch/finally/ purposefully evoke an imperative pattern, make sure they don't interact non-intuitively with other functions, or behave strangely if they're used out-of-order.

Good point. In sync code, try/finally/catch is impossible, but our promises would allow: fn.try(...).finally(...).catch(...). That looks totally weird, but since it's all then under the hood, would work just like you think it would: if a rejection propagated out of the finally(), catch() would indeed catch it. So, I think the machinery is sound, but could certainly be confusing to some devs!

Interestingly, to my eyes, ensure looks a bit better in that particular scenario:

fn.try(...).ensure(...).catch(...)

but finally looks better here:

fn.try(...).catch(...).finally(...)

Athough these still look fairly good, too:

fn.try(...).catch(...).ensure(...)
fn.try(...).otherwise(...).ensure(...)
fn.call(...).otherwise(...).ensure(...)

Since "call" is in "when/function", you would only have "catch" and "finally" by default unless you included the auxiliary module

I think we're ok here. Since finally is on Promise.prototype, which is currently fully defined in the when module, all promises will have catch and finally, even those not generated by fn.try, e.g. when.resolve(123).finally(whatev), although odd, will work.

I bet you're thinking ahead to cujojs/when#95. In that case, if we did allow prototype augmentation, I think we'd want finally to remain in the "core" prototype.

@Twisol

I think we're ok here. Since finally is on Promise.prototype, which is currently fully defined in the when module, all promises will have catch and finally, even those not generated by fn.try, e.g. when.resolve(123).finally(whatev), although odd, will work.

It's mostly just that the expected set of imperative-style functions is missing arguably the most important member. It works, but like you said, it's odd.

@briancavalier

It's mostly just that the expected set of imperative-style functions is missing arguably the most important member

Ah, I see, it's lack of try when not using when/function that is the problem. That's definitely a valid point. We could have aliases, e.g. have both otherwise and catch, but that's just bound to confuse people too. They'll wonder if they're subtly different.

Options:

  1. Keep "call" and "otherwise", and add "ensure". No mental association with synchronous try/catch/finally, which can be helpful for people learning promises. But also no confusion around lack of try.
  2. Pick "try", "catch", and "finally". Will people learn quickly that a "missing" try is not a problem?
  3. Keep "call" and "otherwise", and add "finally". The lack of try and catch may seem weird to people.
  4. Keep "call" and "otherwise", and also provide synonyms "try" and "catch". Provide new functionality with two synonyms "ensure" and "finally".

3 seems odd. Why pick "finally" if we don't have "try" and "catch"?

4 seems like the worst of both worlds rather than the best of both worlds: code worked on by multiple devs over time will become a confusing mashup of synonyms. 4 is out, imho.

@Twisol

Couldn't we implement try simply as .then(onSuccess)? It fits in with the others:

try:     .then(onSuccess)
catch:   .then(undef, onRejected)
finally: .then(onBoth, onBoth)
  // but understanding that onBoth receives no parameters and its result is ignored

EDIT: Fixed the names. Derp.

@briancavalier

try: .then(onSuccess)

We could. If fact, fn.call (which could be named fn.try) is effectively implemented this way (tho it uses spread, which is really just then), but I think the real issue is that there's no need. promise.try(onSuccess) === promise.then(onSuccess), i.e. they are effectively synonyms.

I think "try" makes sense as a way to begin a promise chain, but not necessarily as a way to extend one.

@Twisol

promise.try(onSuccess) === promise.then(onSuccess), i.e. they are effectively synonyms.

True, but try doesn't accept an onRejected handler. That was one of my early mistakes when learning about promises, actually; I expected this to be like try-catch:

p.then(function() {
  // try
}, function() {
  // catch (but not really)
});

I'm leaning more and more towards option number 1: just use ensure.

@renato-zannon

@twisol I've made the same mistake (and still do, from time to time :)).

What if, instead of having separate try, catch and finally, we had one function that comprises them all? That way, we wouldn't have the ordering issue. I'm thinking of something like this:

function try(fn, errorHandler, ensureHandler) {
  // 'apply' being when/function's apply
  var processed = apply(fn).then(null, errorHandler); 
  return processed.then(ensureHandler, ensureHandler).yield(processed);
}

Then, we could do:

fn.try(doSomething, handleErrors, cleanup).then(continueDoingMyStuff);
@unscriptable
@briancavalier

This is def interesting:

fn.try(doSomething, handleErrors, cleanup);

I have some concerns tho:

  1. It looks a lot like then (even tho fn is a module and not a promise) when used this way. This is probably not a big deal and solved via documentation.
  2. Since fn.try would actually need to call doSomething, we'd probably want to allow arguments to be provided. It not clear to me how we can make that work in this signature in a non-confusing way. For example: presumably, you'd want handleErrors, and cleanup to be optional. So that means 2 "primary" optional arguments, plus zero or more "secondary" arguments that would be passed to doSomething

Any thoughts on how we can make that work?

I really love the idea of a when/try module. My only concern is that devs can do this:

var try = require('when/try'); // this would be awesome, but alas

var Try = require('when/try'); // works, but yuck, looks like a constructor

Anyone have any thoughts there? Perhaps when/try is an object with methods rather than simply a function?

What about when.try instead of when/try? E.g.:

var when = require('when');
when.try(doSomething, handleError, cleanup);

That promotes when.try to a more visible place, and (at least perceptively) a more important role than fn.call. Is that good? Also, when.try prohibits the 3 variants we have for call: plain functions, callback-based functions, and node-style async functions. We'd have to name them differently: when.try, when.tryFoo, when.tryBar

If we can come up with something awesome named when.try, and somehow manage the call variants, I'm all for it.

@renato-zannon

+1 for when.try!

The arguments bit can really get a little confusing. But I think it is doable if we think in terms of the apply style:
when.try(func, funcArgs?, handleErrors?, cleanup?);
With the usage being:

when.try(myFunc, handleErrors, cleanup);
when.try(myFunc, [someArg, otherArg], handleErrors, cleanup);
when.try(myFunc, [someArg, otherArg], null, cleanup);
when.try(myFunc, [someArg, otherArg], handleErrors);

It's simple to disambiguate between handleErrors and funcArgs by detecting whether the argument is an array or a function. That way, we don't need to pass null anytime we want to call a function that needs no arguments.

In order to compose custom async try/catch/finally patterns IMHO, we
need a way to easily insert a side-effect into the chain. iiuc, devs
can mimic just about all sync patterns if we just add a way to insert
a side effect.

That makes sense! Maybe what we could have both Promise.prototype.ensure and when.try, with the later being defined in terms of the former.

@Twisol

I don't think I would ever use the arguments feature - there are too many ways between various libraries to associate arguments with callbacks, and ES5 already provides a standard way to bind arguments to a function without calling it. It's also easy to define your own bind (or use a polyfill) if your environment doesn't support bind.

@renato-zannon

Good point @twisol. I wouldn't mind having to rely on partial application or a closure for this either.

@briancavalier

bind is a good option, but I don't feel comfortable mandating it for parameters in this case. I think less experienced devs will not think to use it, and may be confused by it. Also, in limited use so far, I've found fn.call accepting arguments to be useful. Unfortunately, the optional args array doesn't really sit well with me either.

I'm also worried about the signature of this proposed when.try(tryFunc, catchFunc, finallyFunc). It seems too similar to both when, and then. To us it's obvious that it's a different thing, but I fear it will be confusing to others.

Since we already have otherwise (as @riccieri pointed out), and will soon have the thing we are currently referring to as finally/ensure, does it make sense for when.try to simply be equivalent to fn.call?

when.try(myFunc, arg1, arg2, arg3)
 .otherwise(handleError)
 .ensure(cleanup);

The root of the problem, imho, here is the signature of then. The fact that it accepts both onFulfilled and onRejected plays some bad mental tricks. See "[digression]" below.

I may be coming around to @Twisol's earlier comment, i.e. would another path be provide an alternative to then that accepts only onFulfilled. It forces you to handle an error in another chained call to otherwise. That is, when you use this new method, you simply cannot make the then(onFulfill, handleErrorFromOnFulfill) mistake.

(IMHO, it would be nice if we could call this new method then :) but we can't. Again, see digression below)

If we had this new promise method and the first version of when.try above, we might have 2 interesting, and similar, constructs:

return when.try(myFunc, arg1, arg2, arg3)
 .otherwise(handleError)
 .ensure(cleanup);

return promise.someName(myFunc) // This only accepts onFulfilled
 .otherwise(handleError)
 .ensure(cleanup)

I'm kind of liking that.

Also, we have adapters for turning callback-based and node-style async functions into regular functions that return a promise. That means people can use when.try to call different kinds of functions by using an adapter. It also means we can write more adapters in the future as needed, and those will work with when.try as well.

// Note that the `path` arg can be inside of `nodefn.bind` as well and it would work the same.
when.try(nodefn.bind(fs.readdir), path)
 .otherwise(...)
 .ensure(...)

[digression]

Why I think then(onFulfilled, onRejected) is a mental trap:

  1. It tricks some people simply because onRejected is to the right of onFulfilled.
  2. More importantly, it lures one into thinking of fulfillment and rejection as 2 symmetrical end states of a promise. This is not really true, though. A better way to think about it is that a promise can be either fulfilled or not-fulfilled, which is subtly different from fulfilled, pending, or rejected. When it is not-fulfilled, it is either in the "fulfillment is possible", or "fulfillment is impossible" state. If you think about synchronous functions, their primary responsibility (and thus their desired end state) is to compute and return a result. In pure mathematical functions, there is no exception throwing. So, returning a value and throwing and exception are not 2 symmetrical things. Throwing an exception is a way to indicate that the primary goal (returning a value) is now known to be impossible. Similarly, a promise becoming rejected indicates that the primary goal (fulfillment) is now known to be impossible.

[/digression]

@Twisol

So, returning a value and throwing and exception are not 2 symmetrical things. Throwing an exception is a way to indicate that the primary goal (returning a value) is now known to be impossible. Similarly, a promise becoming rejected indicates that the primary goal (fulfillment) is now known to be impossible.

Philosophically, perhaps so, but handlers further down the chain can switch back from the failure path to the success path. I prefer to look at a promise chain as a unified pathway for a stimulus to travel through; the most trivial, undecorated kind of asynchronous processing is just composition of a set of functions without invoking them.

What when.js does, with its splitting of the path into success and failure, is create a sum result type. In the style of Haskell: data Result x y = Success x | Failure y. I like this a lot! return becomes Success, and throw becomes Failure. And if Haskell isn't mathematically pure enough, I don't know what is. :laughing:

I totally agree with point 1, but it really does make sense to view fulfillment and rejection as symmetric. They just construct disjunct elements of the sum type.

@briancavalier

In my view, it's really the same thing, with different machinery. The primary goal of a function that returns Success x | Failure y is to compute a successful value ... the primary goal is not to "compute a failure". Again, IMHO, the failure is simply an indication that success was impossible.

It was my fault for derailing the thread with my digression. Sorry! @Twisol, I'd be happy to continue discussing this success/failure concept via IRC, so feel free to ping me there.

@briancavalier

Let's get this thread back to focusing on a decision about always and the related try stuff, which we can split off to another issue if the discussion continues for much longer.

To that end, everyone please post your latest feelings on:

  1. otherwise + ensure
  2. catch + finally
  3. Something else

Any your thoughts on:

return when.try(myFunc, arg1, arg2, arg3)
 .otherwise(handleError)
 .ensure(cleanup);

return promise.<someName>(myFunc) // This only accepts onFulfilled
 .otherwise(handleError)
 .ensure(cleanup);

vs.

return when.try(myFunc, handleError, cleanup); // Remember, would have to .bind() the args also

Heh, writing that out sure is nice, cuz it's so short. Still concerned about confusion around the signature and confusion with when/then, tho :/

@Twisol

I like otherwise and ensure because it doesn't shake things up too much. catch and finally carry imperative connotations which, while more or less accurate, don't mesh with the functional approach taken elsewhere in the API.

return when.try(myFunc, handleError, cleanup); // Remember, would have to .bind() the args also

This reminds me a lot of C#'s with statement, actually. From that perspective, I rather like it. (Perhaps call it with?)

@Twisol

I derped up - I meant using from C#, not with. Sorry for any confusion.

@briancavalier

@Twisol, "using" is a good word. Do you mean:

when.using(myFunc, handleError, cleanup);

For my non-C# brain, "using" is less intuitive than "try" in this particular case, but would like to hear what others think.

@Twisol

Hmm. My attempts at explaining it leave me wondering what I was thinking. using is a nice concept, but semantically very different from what we're actually dealing with here. So... nevermind!

@unscriptable
@briancavalier

Because it would actually "try" to call myFunc with those args, not simply bind/partial.

@unscriptable

Sorry, that's making no sense to me at all. What does that have to do with the sync try-catch-finally construct?

@briancavalier

If we had catch and finally, you'd be able to do this, which is somewhat (but not exactly) parallel to sync try/catch/finally.

return when.try(
    doSomething, x, y
).catch(
    handleError
).finally(
    cleanup
);

We don't have to name them "try", "catch", and "finally", especially since we already have "otherwise". FWIW, you can almost do this now using when/function:

var fn = require('when/function');
return fn.call(
    doSomething, x, y
).otherwise(
    handleError
).somethingThatIsNotTheExistingAlways(
    cleanup
);
@unscriptable

Maybe I missed something. Does when.try() return an object with only two functions -- catch() / finally() or otherwise() / ensure()? If so, this makes sense to me. Otherwise, if when.try() returns a promise and I could potentially when.try(func).then(other) then that makes no sense to me at all. In the latter case, I see no reason to call it "try". It's just a way to call a function with the supplied args.

@domenic

Big fan of try/catch/finally; matches Q as well. Whatever somethingThatIsNotTheExistingAlways ends up being named, I think the finally alias should be available (although it will probably need another name for pre-ES5 browsers like PhantomJS).

@briancavalier

Appreciate the input, @domenic. There are concerns among the folks here over less experienced devs being confused by the sync implications of the words try/catch/finally. I like them, but also understand the concerns.

I'd be interested to hear what @unscriptable, @riccieri, and @Twisol think about providing these as aliases. I'm usually a fan of providing only one way, but it's worth asking.

@Twisol

I think overall, I'd prefer seeing a simple ensure added. I'm not convinced that there's significant value added by implementing new try/catch/finally functions. They look elegant on the surface, without a doubt, but I haven't been convinced that you can't do the same things just as easily without them.

@unscriptable
@domenic

I think the whole point of this thread is that it's not. It gives semantics more like a finally block, passing through the existing fulfillment value/rejection reason. See Q's implementation

@briancavalier

promise.catch(err).finally(doThat).try(wat).then(...);

There's no proposal for promise.try(). It'd be weird, like you showed.

There is a proposal to have some method, somewhere, named try, which might be a synonym for the current call() in when/function. It's clear that something named try needs it's own discussion thread! I'll start one. Let's stay away from it in this thread for now.

Isn't ensure(doThat) is really just a shortcut for p.then(doThat, doThat)

Thankfully, no :) p.then(doThat, doThat) is exactly p.always(doThat) as of 1.8.0. The original purpose of this issue was to create something better (the problems with the current always are documented in the original issue description). One proposal is to all this new, better thing "ensure". Here is a test implementation of how this new thing would work. I named it "finally" in my test because we hadn't yet discussed the word "ensure".

We have a good implementation for this new thing. We just need to name it.

@unscriptable

Ok, my bad. Thans for reminding me. :| Still, my feeling haven't changed one bit. I understand that this method can be used to asynchronously mimic the typical use case for finally, but that's not all it's good for. It's generally good for inserting async side-effects into a chain while also preserving the existing fulfillment value or rejection reason.

That feels more like a "detour" or an "aside" than an "ensure" to me.

promise.then(foo).aside(doThat).then(ok, err);

If we want to help devs mimic the semantics of their comfy try-catch-finally, then I think that is much better done in a way that doesn't confuse them with the myriad combinatorial possibilites. I'll post my feelings to #115 like a good github citizen. :)

@briancavalier

Ah, "aside" is another good word. I think "ensure" doesn't bother me--for whatever reason, my brain seems ok with other things coming after "ensure". To me it just means "ensure this thing gets done right here, no matter what". These two both look nice to me:

promise.then(foo).aside(doThat).then(ok, err);

promise.then(foo).ensure(doThat).then(ok, err);

Of course, I still like "always" :/

promise.then(foo).always(doThat).then(ok, err);

One nice thing about "aside": it's an adverb, like "then" and "otherwise", whereas "ensure" is a verb. Also, finally doesn't have to be the "last thing", so finally(cleanup).then(doMore) actually does have a sync equivalent:

try {
  doSomething();
} catch(e} {
  recover(e);
} finally {
  cleanup();
}

doMore();

That said, I don't think we have enough support to call it "finally" right now.

@unscriptable

I like the words "ensure" and "always", but they don't tell me that the resolution value or rejection reason will pass through.

@briancavalier

Good point! Hmmmm

@briancavalier

This usage is a little weird:

return promise.then(doSomething)
  .otherwise(recover)
  .aside(cleanup); // my brain expects a different word here

Hmmm, this makes me think we actually have 2 different concepts here--whether we choose to embody them both in the same construct or not, I'm not sure:

  1. Something to insert a side effect at any particular point in the chain. "aside" is a nice word for this. One question: does it imply that the operations following it will indeed wait for it to complete, or does it imply parallelism?
  2. Something that "feels" like it should be used like "finally". A bit nebulous, I know, but people know how to use finally, so it makes me think they would learn how to use some async analog.
@pcad

@unscriptable I like the point about the resolutions passing through. What about 'regardless'?

@briancavalier

Hey @pcad! I like that "regardless" is in the same vein as "otherwise".

FWIW, I ran into a perfect use case for this while working on #116. I ended up with some code that looks like this:

return when(wait(), function() {
    return f.apply(undef, args);
}).then(
    function(value) { notify(); return value; },
    function(reason) { notify(); throw reason; }
);

Yuck. I'd really rather write something like:

return when(wait(), function() {
    return f.apply(undef, args);
}).ensure(notify); // finally, always, and ensure make sense here to me

How is "ensure" sitting with everyone?

Here's another thought on "tap". Would it make sense to have something like:

Promise.prototype.tap = function(tapFunc) {
  return this.then(function(value) {
    return when(tapFunc(value)).yield(value);
  });
}

In other words, it is a side effect that is allowed to observe the success value but not modify it, i.e. it "silently" taps into the fulfillment chain (like "wire tap"). Note that it still could return a promise to delay subsequent steps in the chain (but still couldn't change the value)

Hmmm, what are the use cases for this "tap" thing?

@briancavalier

After more thought and discussion, we decided to go with ensure. It's going into 2.0, and always will be deprecated.

@briancavalier briancavalier referenced this issue from a commit
@briancavalier briancavalier Close #103. Implement promise.ensure, deprecate promise.always. API d…
…ocs for ensure, switch unit tests to use ensure(done)
9e29a66
@otakustay otakustay referenced this issue in ecomfe/er
Closed

重新整理Deferred的API #28

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.