Closed paulmillr closed 11 years ago
Thanks to the Promises/A+ folks for asking really good questions, and thanks to @pufuwozu (and others) for answering with lots of good information and examples. I'm basically in favor of anything that makes promises "better" and more useful, without also making them less approachable/learnable.
My main concerns are:
Approachability/Learnability - it's been the experience of many Promises/A+ contributors that Promises, in general, already are daunting to developers. It's at least been my experience that Monads, in general, can also be daunting to folks. I think that we need to be careful not create something that is even harder for developers to grok/use. As a super simple example, I fear that the name point
is not intuitive for what it does, and would confuse many developers. It's great that @pufuwozu has said over in #24 that he's cool with one of our previously proposed names from
. If monadic promises can be theoretically strong, but also no more difficult to learn, I think that kind of synergy of theory and practicality can be a big win.
Interoperability - there's a substantial and growing amount of deployed code based on non-promise thenables, Promises/A, and Promises/A+. I think we have to consider that, and make sure we don't break the world. For example, changes to the signature and/or behavior of then
could be a big barrier adoption, or worse, could cause developers some serious headaches. Again, it seems like it's possible to move forward in a way that maintains compatibility/interoperability, but it's something we need to keep in mind as we explore this approach.
Promises/A+ 1.1 timing - This version is effectively "done", and I think we should release it. We can continue to explore monadic promises for a subsequent version.
DOM Futures - It's not clear to me how this could/would/should influence the current DOM Futures work, but maintaining compatibility seems very important. I'm hoping someone more involved with that effort (@domenic, @erights, @wycats?) is able to comment.
It would also be good to hear from other lib authors, especially @kriskowal, @erights, @wycats, @lsmith, and @novemberborn.
@briancavalier As far as approachability/learnability goes, I believe Monads provide a much simpler interface for Promises/A+ (even ignoring the "being more composable" argument, which is a rather strong one). I think we should value simplicity over a seemingly easy but complex API (plus we get more compositionality yay!). The current specification for then
is really complex, it merges lots of concepts and deals with a fair bit of automatic conversion for the user. While this does allow a particular user "type less" in the general case, it's not something that really scales on the long run (see the Abstract Equality Operator).
People don't need to be concerned about what Monads are or aren't to use Promises. I think that's an important part of this discussion I don't recall people mentioning before. Users who don't get Category Theory, Monoids, Monads, Functors, Applicatives and all that stuff (which would include myself for now :3) can just look at this whole thing as more combinators. Functional idioms are beginning to spread throughout the JS community, even though people don't grok all the internal details of those idioms: function composition, generic and specialised folds, etc. So I think it's worth a shot working together to construct an API that is simple (by the means of separating and composing concerns instead of overloading things), and still uses a vocabulary that is slightly more familiar to JS developers to help with adoption.
Interoperability is a problem. For example, by changing the behaviour of then
and making it simpler, we will have to ask all previous implementors to adapt to the new specification. Abstractions built on top of those might also need to some rework. However, sometimes breaking backwards compatibility can be worth it.
Just got reminded by @dherman - anything with a then
is a "Thenable" which is treated specially in the spec.
If we also put a then
on Array - things would seriously break when an array is returned from a function passed to promises libraries!
There's two choices:
then
method can be treated as a promise Thenablethen
method shouldn't do special things when value returned from the function doesn't have a then
- we can't have libraries rely on that behaviourOne of these things really needs to change to allow DRY code.
To be exhaustive, here's two more choices:
Array.prototype.then
I know some people will reach for that last option but I don't want to give up :smile:
OK. So. People seem to not have understood how bad of a mismatch @pufuwozu's post was for Promises/A+, both in content and in tone. Let's try to clarify that, and hopefully make it clearer why I stand by my original post.
First, coming into a community and saying "you're doing it wrong! look over here!" in a post filled with technical inaccuracies and misunderstandings is rude and unwelcome. Compounded with well-known troll @paulmillr, of racist CoffeeScript pull-request fame, as the messenger, you can see how this is problematic.
Now, let's get down to brass tacks. Although @pufuwozu has backed off on his onFulfilled
-must-return-a-single-type argument, the general perspective from which such a demand comes from is unrealistic, and indeed stems from a typed-language fantasy land where you can enforce such things and don't have to think about the possibility of receiving an unwelcome type. Indeed, his suggestion to leave returning non-promises unspecified is ridiculous for a JavaScript specification.
Furthermore, the problem isn't only a narrow typed-language worldview. It's a narrow pure-functional language worldview as well! Promises are an implementation of the monad pattern that helps to encapsulate an exception channel into the monad. That much is, I hope, obvious to all. But a key part of how promises implement this pattern is by embracing the imperative nature of the language, and automatically translating return
(of non-thenables) and throw
into the appropriate promise representation. Any implementation of monads that is "right" for JS will necessarily embrace these imperative possibilities, instead of ignoring them.
Finally, there seems to be a crucial misunderstanding of what the Promises/A+ spec is meant to do. It is not meant to dictate a standard for all aspects of promises in JavaScript, and it is certainly not meant to provide a standard for monads in JavaScript. Instead, it is something that grew out of many years of implementer experience, and the need to provide a minimum interoperable specification of the then
method. As such much of its complexity is focused around interoperability (see the entire Promises Resolution Procedure). Remember:
A standard for sound, interoperable JavaScript promises—by implementers, for implementers.
This is why decisions like promise creation are not part of the core specification, while in contrast, we do carefully specify thenable assimilation.
I think the key here is something that grew out of many years of implementer experience, and more importantly many years of JavaScript experience. We have found an appropriate implementation of a specific monadic pattern in JavaScript, and that includes things like how we handle exceptions, how we handle polymorphic return values, and how we duck-type other implementations of the same pattern. This also feeds into discussions of flattening promise chains; it has previously been discussed over and over that a promise-for-a-thenable is a problematic pattern in JavaScript and should not be supported. (A promise for another monad, of course, is fine.)
In contrast, nobody has spent years implementing algebraic patterns in JavaScript. If they had, I think we'd be having a very different discussion right now, with much less focus around Haskell idioms and much more focus around those types of issues. The insistence of some people in this thread that the Haskell (or Scala, or...) implementations of these algebraic patterns is what we should do is very misguided.
I fully support any efforts to try to come up with some standard way of expressing monads and other algebraic concepts in JavaScript. Preferably, it should embrace the language instead of trying to make it conform to patterns of languages from other paradigms. But that's not something I am interested in spending time on as part of Promises/A+. I think a separate GitHub organization, perhaps with overlap from those who seem interested, is the right place to do such work.
In the meantime, Promises/A+ has no intention of incorporating whatever nascent "monad standard" such an organization comes up with. Neither, I think I can safely guess, does the DOM Standard and their DOM Futures.
Indeed, I think the standardize-first approach is the wrong one; it is certainly the opposite of what we have done with Promises/A+. Rather, I'd suggest going off and building libraries, with appropriate wrappers---one for Promises/A+ promises; one for arrays; one for an option type implementation; etc. If you have, after a few years of experience, managed to create a pattern that is widely used in libraries as popular as jQuery, Ember, Angular, WinJS, and more (perhaps with some flawed implementations, like jQuery's, lol): only then should you write up a spec, and a test suite. You need those years of experimentation to determine what the spec should even say.
Finally, after those years of experimentation, and the subsequent months of hammering out a standard, then come back to us. Maybe, if you've built an extremely compelling "algebraic JavaScript" ecosystem, Promises/A+ would consider a 2.0 revision in which we effectively tell all Promises/A+ implementations that the benefits are compelling enough that they should implement these new APIs as well. Most likely, if you're indeed that successful, most Promises/A+ implementations will already have adopted your patterns, and it'll just be a matter of codifying de-facto reality into something with all the edge cases smoothed out---like we have done with Promises/A+ 1.0 and 1.1.
@domenic
it has previously been discussed over and over that a promise-for-a-thenable is a problematic pattern in JavaScript and should not be supported.
Except that the current behaviour of then
, while allowing for compatibility with all sorts of previous work, is confusing, and rather prone to break exactly because of the overloading.
Someone returns a thing that has a then
method and is fed into a Promise function? Broken. Someone use a Parser Combinator library that uses then
for sequencing actions but no promises, feeds that into a promise-expecting function? Broken. How does the user reason about that? Well, he doesn't, and that's what is really terrible about Promises/A+.
Specially if Promises/A+ gain enough traction to be used everywhere as they should, consider:
var compileP = liftNode(compile) // compiles AST to something
var flattenP = liftNode(flatten) // flattens and optimises AST
var parseP = liftNode(parse) // parses text, returns AST
var source = fs.createReadStream('source.foo')
var compileSourceP = compose(compileP, flattenP, parseP)
spit(process.stdout, compileSourceP(slurp(source)))
Now, consider spit
is a function of type spit :: Promise Stream, Promise String -> Promise ()
, and slurp is a function of type slurp :: Promise Stream -> Promise String
. parse
, flatten
and compile
are straight-forward in meaning, but they are not functions that know about promises, they are just wrapped in a function that transforms the arguments into a Promise, applies the function, and wraps the return in a Promise.
The problem now is all of those functions need to use then
to wait for a promise to be resolved. Let's say the AST
happens to have a then
method for whatever reasons, this means we wouldn't be able to write that code, because flattenP
would try to assimilate the return of parse
, which is not a Promise.
See also @pufuwozu's comment above yours.
Is @pufuwozu filled with Haskell's dogmatism? I don't think so. Could he have worded his proposal better? Perhaps. Does he raise good points that we should evaluate? I think he totally does. Even if we disregard the whole "monads-category-theory-functional-programming-narrow-minded-pure-theoretical-ideas", there are some problems with the specs right now, which will mean things are gonna be fucked up as people begin to use them more and combine them with regular values. This is made worse by JS's lack of type constraints.
Indeed, I think the standardize-first approach is the wrong one; it is certainly the opposite of what we have done with Promises/A+.
It's also one of the things that bring in some terrible incidental complexity because now you have to support all of the previous work on this front. Same happened with ECMAScript. And is happening with ES6 again. Accidental complexity is never something we should strive for, imho.
As a side note, the tone of this discussion is really not helping. Fights means people get more defensive and less open to other people's ideas, which is, I believe, not what we want here(?).
@domenic my implementation of point
is wrong - where are my other technical inaccuracies and misunderstandings? You haven't commented on them.
... and it is certainly not meant to provide a standard for monads in JavaScript.
It's not - but let's at least make it compatible! What was the reason not to?
Furthermore, the problem isn't only a narrow typed-language worldview. It's a narrow pure-functional language worldview as well! ...
That whole paragraph has nothing to do with what I've been talking about. The API that I derived is not pure at all. Thinking that this has anything to do with purity is absolutely wrong. I have no idea where you got that idea.
I think the key here is something that grew out of many years of implementer experience...
You can't just say "we've spent years implementing X, so it doesn't have to do/be Y" - that's absolutely useless. Give a reason, not an argument from authority.
... and more importantly many years of JavaScript experience.
I have been writing hobby JavaScript for 11 years - professionally for quite a few of those years. I write a few serious JavaScript libraries. I'm not coming in here from outside of the JavaScript community (not that it should matter, at all).
I also use many other languages - does that make your JavaScript experience more important or something?
We have found an appropriate implementation of a specific monadic pattern in JavaScript, and that includes things like how we handle exceptions, how we handle polymorphic return values, and how we duck-type other implementations of the same pattern.
They're not monadic - that's the whole point. What you're doing could be monadic - it's very close!
Preferably, it should embrace the language instead of trying to make it conform to patterns of languages from other paradigms.
Why do you have this crazy idea that because Haskell recoginises that monads are monads, then we're trying to make JavaScript like Haskell? You're being very dishonest.
The API that you can derive makes sense algebraically. This has nothing to do with Haskell or Scala.
In the meantime, Promises/A+ has no intention of incorporating whatever nascent "monad standard" such an organization comes up with.
Useless.
Finally, after those years of experimentation, and the subsequent months of hammering out a standard, then come back to us.
Wait, so is it absolutely no or maybe?
Come on. I read all of that for nothing - there's no content!
I have not seen any reasonable excuse against recognising that promises are monadic. You've basically come up with "it's pure" (incorrect), "it's from Haskell" (incorrect), "it's not from JavaScript" (that's what I'm trying to fix) and "implementors made this - we don't need your input" (urgh, thanks).
Anyway, I'm going to reiterate our 4 choices:
then
then
which does special things for when returned with no then
Yes, all the options suck. Problem is that I think #4 sucks the most.
@killdream the issue of things which have .then
methods but aren't "thenables"[1] is vastly overstated. I haven't seen a single issue on GitHub about a bug resulting from this behavior. Given that I work on projects that use promises (both my own and other people's) almost every day, I would've seen hundreds if this was a significant problem. It just isn't.
[1] thenable: (something close enough to a promise to be successfully assimilated
@ForbesLindesay I added a then
method to Array
. Everything broke. Want to file an issue for that?
OK, I've now seen one person with a problem resulting from this, and you extended the prototype of a built-in. There are exactly 2 reasons why that would be acceptable:
@ForbesLindesay urgh, missing the point.
I just wrote a wrapper around Array
and added a then
method. Everything broke!
@ForbesLindesay I'm okay with assimilating promises when you return a promise from a function in an on{Fulfilled,Rejected}
, I'm not okay with assimilating things with a then
function. That it hasn't happened until now doesn't mean it will never happen (potential ≠ guaranteed). For example, null
is a terrible API design, everyone agrees with it, but even so null
references are only "potential" problems.
Specially problematic with things that looks like thenables but aren't quite. Duck-typing is not the way to go here because the semantics of a Thenable
are really important for them to be assimilated successfully, and these kind of errors might not exactly be early errors, which is more of a problem because now you have to step through your whole code base to figure out what went wrong. Dynamic branding would be a better option to mark compliant promises specifically and only assimilate those. Then the user has to go through all the trouble to wrap foreign things that are close to promises, which ensures they won't shoot themselves on the foot by a seemingly innocent code like the one in my example above.
@ForbesLindesay I wrote this:
function Id(a) {
this.value = a;
}
Id.of = function(a) {
return new Id(a);
};
Id.prototype.constructor = Id;
Id.prototype.then = function(f) {
return f(this.value);
};
Everything broke!
@pufuwozu seems to be arguing that Promises/A+ ought to be compatible with monads because it only requires a small change and doesn't have any significant drawback.
@domenic seems to be arguing that the drawback is insurmountable, but I'm not seeing where this is demonstrated.
Indeed, his suggestion to leave returning non-promises unspecified is ridiculous for a JavaScript specification.
Why is this ridiculous? jQuery doesn't specify what happens when you pass a hundred arguments to $(foo).siblings - does it need to? JS is a dynamic language. There are all sorts of unspecified legal actions; as JS authors, we are used to that.
Compounded with well-known troll @paulmillr, of racist CoffeeScript pull-request fame, as the messenger, you can see how this is problematic. I think the key here is something that grew out of many years of implementer experience, and more importantly many years of JavaScript experience.
Serious question: when people argue with you using ad hominem attacks and bludgeoning you with their credentials, does that make you more likely to think that (A) they are probably right after all, or (B) they are digging in their heels for pride rather than what's best?
Please reconsider.
@killdream "Let's say the AST happens to have a then method for whatever reasons," Don't do that.
The original E promise API which inspired all these others has no assimilation. When I first heard about assimilation, I thought it was a terrible idea, for many of the same reasons that underlie many of the above objections. However, as I saw the JS promise landscape evolve, I despaired more over a worse problem:
Several similar but different elephants in the room: jQuery promises, WinJS promises, Q promises, and DOMFutures. All of these but DOMFutures have enough installed base that they're not going away. And re DOMFutures, never underestimate the ability of the w3c to be an elephant even for premature "standards". Much code will have to be written in an environment inhabited by multiple such promise systems simultaneously. If none of these recognize each other's promises as anything promise-like, then programmers will face the burden of dealing with a "jQuery promise for a Q promise for a WinJS promise for a DOMFuture for a number". Call it the JQPFAQPFAWJPFADFFAN problem. I didn't see how we could get from that situation to one with an agreed standard promise anything.
Fortunately, the starting point for all of these elephants was so similar that the Promises/A+ process was able to tease out and codify a common-enough ground for all of these to agree on. This is messy real-world standards work; as much politics and sociology as technology and math. Given the constraints imposed by legacy, we should all be overjoyed that the Promises/A+ community has been able to extract something as beautiful as they did. But even if they all agreed on exactly the same spec, so long as jQuery promises only recognize jQuery promises as promises, and likewise for the others, we would still have the JQPFAQPFAWJPFADFFAN problem.
Had ES6 with its unique symbols happened long before any of these promise libraries, we could have used a unique "@then" symbol for assimilation and so avoided the accidental collision @killdream worries about above. Likewise, if all of these had stuck with the E (or original Q) name "when" rather than renaming it "then", we would have less accidental collision, and would be less irritating to category theorists. (FWIW, I think "when" was a much better term than "then" for this anyway. I am still unclear on the history of how this got renamed.) Alas, it didn't turn out that way.
Given multiple promise systems that each recognize only their own promises as promises, but which mostly agree on the meaning of "then", assimilation turns out to be a surprisingly pleasant way for these to co-exist. Given the actual situation we have to start from, having this assimilation be based on duck typing on the presence of a function named "then" is unfortunate, but there was no other practical choice.
@rtfeldman the comparison with jQuery is completely out of place here for two reasons:
A fair comparison would be the ECMA Script spec, which almost always specifies these corner cases (the only reason it would ever not do so would be by mistake.
@pufuwozu Your Id object:
function Id(a) {
this.value = a;
}
Id.of = function(a) {
return new Id(a);
};
Id.prototype.constructor = Id;
Id.prototype.then = function(f) {
return f(this.value);
};
Looks plenty enough like a promise to me, when I assimilate it using a true Promises/A+ library I get a promise which resolves to the value of the Id. That's well specified, well defined behavior. I've found it helpful hundreds of times, and never found it a problem, and nor has anyone else.
@killdream
never happen (potential ≠ guaranteed)
Yes, but JavaScript is not a language that concerns itself greatly with proving things. Pragmatically speaking, I don't think it will cause anyone problems unless they are trying to abuse promises to make them more like Haskell Monads, as such, I don't think it's a problem. We considered "branding", but that ultimately causes the same problems, just one down the line: "It doesn't matter how many _
s you add to the start of a property in JavaScript, it's still public".
@ForbesLindesay Fair point that jQuery isn't a spec.
Here's the ECMAScript 5.1 spec on Array.prototype.reduce
: http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.21
- If len is 0 and initialValue is not present, throw a TypeError exception.
This is the ECMAScript spec saying "if you pass an empty array and no starting value to reduce
, that doesn't make sense, so TypeError."
Granted, explicitly specifying "throw a TypeError" is not the same thing as leaving it unspecified, but this is certainly an example of a JS spec saying "you can't use certain types in places where they don't make sense," no?
@rtfeldman effectively saying "you can't use certain types in places where they don't make sense," by deterministically throwing a TypeError is fine. Unspecified isn't. Care to rephrase what you had in mind in as a deterministic spec?
I'm not certain that I understand the problem here. As understood, the following are equivalent:
p.then(onFulfill, onRejected)
and
p.then(onfulfilled, undefined)
p.then(undefined, onRejected)
indeed, many implementations provide single-function shorthands. And of course, a full two-argument could be built from those single-function shorthands, thus
function then (onFulfill, onReject){
p.shorthandFulfill(onFulfill)
p.shorthandReject(onReject)
}
It would seem, then, that nothing in the Promise/A+ spec precludes the use of a promise as a monad, and the monadic promise proposal does not preclude the use of a promise as a practical notation for asychronous behavior. Given that Javascript and the spec provide that .then(oneCallback) operates the same under each proposal, the debate seems unnecessary.
Why choose?
The question is only how the notation for the spec should be accomplished. I agree with the overarching sentiment that we should describe the specification, so much as possible, using notation that users and developers understand. In this way, clients can read the spec and comprehend what is happening. Category theorists can use the spec trivially and without translation. And there is no reason why it can't be pointed out that distinct uses of the .then functions result in monadic behaviors.
The .then position is only one of a few pieces of the proposal, but I simply don't get this piece at all. I will write separately about the other issues.
@ForbesLindesay @pufuwozu's example breaks depending on the assumptions the developer has when he wrote the Id
constructor. If he doesn't expect it to be assimilated by a Promise library, it's broken.
@erights Legacy and incompatible things are unfortunate, which was I said all that stuff about accidental complexity. I believe explicitly wrapping non-conforming implementations would be best. Have a function like promiseFromThenable(a) :: Thenable a -> Promise a
the developer can use to explicitly glue alien functions together, but I haven't really considered edge cases.
My example with the AST was based on the assumption I don't control that library. @polotek seems to have a Node library with a then
method that isn't a promise. Promise-expecting functions can't use that library as a result.
@ForbesLindesay I actually think the probability of these kinds of things increase as you use Promises as actual Promises, because then you'll be mixing them with all the sorts of regular values, and it won't be apparent from your code base what is a promise and what isn't. If I omitted most of the definitions of my example, it would be one of those cases:
// is `source` a thenable? Is the result of `slurp? The result of `parse`? The result of `flatten`? ...
spit(process.stdout, compile(flatten(parse(slurp(source)))))
@polotek Please fix your Node library. Thanks in advance.
@killdream yes, until @polotek fixes his library, it can't co-exist with various promise libraries. It is far too late to do anything to fix that. The evolution of JavaScript has always been and remains constrained by its history of usage.
@wizardwerdna "p.then(onFulfill, onRejected)" evaluates to a promise for the value that the invoked callback will return. What does "p.then(onfulfilled, undefined); p.then(undefined, onRejected)" evaluate to?
@killdream
I use promises is in all sorts of large real world applications. Some of which are closed source, but there are some notable exceptions such as esdiscuss and the now deprecated component-website. You just don't end up suffering these issues.
As for your idea of forcing people to explicitly convert thenables into "Promises" it simply doesn't work. There is no sure fire way to tell the differences between a Promises/A+ promise and a thenable. Branding would get close, and was considered, but ultimately rejected. Search through the issues if you want to see more.
@wizardwerdna
There's a slight problem, you're neglecting the return value of then. p.then(onFulfilled, onRejected)
is actually equivalent to:
(p will be fulfilled) ? p.then(onFulfilled, undefined) : p.then(undefined, onRejected)
Unfortunately p will be fulfilled
is uncomputable
Another piece of brian's article goes to his "point" function. This functionality already exists in several libraries, typically named "coerce" and sometimes referred to as "assimilate". Indeed,the newest spec requires an "abstract procedure" called Promise Resolution Procedure, which is often manifest in code as resolve(thingie)
. This is nothing more than exposing resolve
to the API. On the other hand, since it can easily be coded from the already-specified API, why does it matter?
Indeed, let me ask this question. Since the monadic use of Promises/A+ can be built completely from the spec, why not consider it simply to be a mixin that makes a Promise into an MPRomise?
erights, you got me. How about
p.then(onFulfilled, undefined).then(undefined, onFulfilled)
This returns a promise for the state and value that the invoked callback will return, albeit through a chain of promises via the Promise Resolution procedure.
That one is much more obviously different. Consider what happens if onFulfilled
throws an exception.
The nonstarter in Brian's proposal, in my view, is the following:
I think then should only take the onFulfilled function and it should have unspecified behaviour if the function doesn't return a promise (for simplicity; map can be derived).
I simply don't think that we are all that hung up on types here in the land of javascript, and the /A+ specified behavior seems intuitive to me. Since Brian doesn't care (his definition permits the existing usages), the /A+ spec seems to do what he wants, just more.
@wizardwerdna well, it's tricky to explain.
If we have other things that have then
because they're all monads - then adhearing to the promises spec promises that you'll be stuff when I pass a monadic Array in.
If the promises spec doesn't specify that it should fall back to map, then some libraries can reject that behaviour and give early errors.
It's not about types.
@ForbesLindesay
Perhaps I was unclear. Considering the chained expression, as compared to the two-parameter version, doesn't the return promise in each case end up rejected with the error of the onFulfilled call.
@wizardwerdna: In your chained example, if the onFulfilled callback fails, then it will be called again, but with an error instead of whatever was originally passed.
@pufuwozu Brian, trying to follow. Can you give an example? As understood, you can still wrap the old library behaviors with a coerce function.
Invariably, you will ordinarily try to use just one library anyway, resorting to other objects only when, for example, an I/O or Ajax library returns an ugly out-of-library "thenable". But its a trivial matter to use the coercion procedure for any return value, or better yet, to define a library-version of the external API calls, returning "legit" promises.
My point is that the present Spec seems multi-purpose, and the limitations on their use proposed by Brian seems easily built atop it. If I am not wrong, what's the beef?
@Twisol OK, I get it. The two-parameter version will only ever call the first callback, and the then-promise rejects with the error. The two-step version, errors out, so the then-promise rejects with the error, triggering the second callback. This is a semantically important consequence of the one-callback-only limitation of the spec.
Thus, I agree that we don't have equivalence, in that the monad can't seem to readily reproduce the A+/spec version. But doesn't it work fine the other way, in that the monadic version can be replicated by the spec? Thus, why the sadness in the monad community, since the spec already gives them what they want. Defining monadThen
as function monadThen(cb){specThen(cb);}
and monadOnRejected
as function monadOnRejected(cb){specThen null, cb);}
@wizardwerdna so here's my "monad spec" for JavaScript:
A monadic value is something that:
then
takes a function which takes a value and returns a monadic value of the same constructor - then
also returns a monadic value of the same constructorconstructor.of
creates a monadic value of the same constructorIt also has to obey these laws:
of(a).then(f)
is the same as f(a)
m.then(of)
is the same as m
m.then(f).then(g)
is the same as m.then(function(x) { return f(x).then(g); })
Great, promises implements part of this specification (and hopefully more, soon). Awesome.
I make my Id
constructor implement the above monadic specification - now I pass it to a promise library. Uh oh, it also has then
so promises thinks it's a Thenable and does strange things with it. It's buggy.
It's not about promises not being part of "the monadic spec" - it's that promises will break when there's more than just promises involved.
Should I move forward with implementing many things with then
and just let promises break? Or should promises change to not let this happen?
Does that make sense now?
@pufuwozu Don't define then methods that don't conform to the Promises/A+ spec. If you want a then-like method with a different behavior, call it something else.
@erights but Promises/A+ already provides a compatible then
method. They are close to monadic. It's a problem with promises if they do strange things with other monads. They shouldn't.
It's really sad that @erights highlights how tied we are to a stripped version of structural typing where every superset of { then :: Function }
is considered a Promise
=/
@wizardwerdna: Yes, that's right. Since the 2-ary then
is strictly more powerful than then
and otherwise
, you can implement both of them in terms of the former. I do think that the 2-ary then
should be called something else, but that ship sailed a long time ago.
The thing is, there are other things in Promises/A+ that you have to be careful of. Despite @pufuwozu's statement that "it's not about types", there are a lot of design tradeoffs based on the very fact that you can't differentiate types beyond a certain level of complexity. Have a single type you want to handle specially? Great, use instanceof. Have a whole range of types that have to implement an interface? That kind of check, in Javascript, ends up with problems.
And unless you want to use a transpiled Javascript language, there are no good solutions to the problem which also keep things at an approachable level. I admire @pufuwozu's approach to functional techniques in bilby, but it forces a total paradigm shift that most developers won't want to deal with. We could refactor then
into separate bind
- and fmap
-alike methods, but in JavaScript that just makes them more brittle.
@pufuwozu:
It's a problem with promises if they do strange things with other monads. They shouldn't.
If we had a stronger type system, the Promise methods wouldn't even accept other monads as input. Functions that are total in statically typed systems are partial in JavaScript - and we don't even have a good way to restrict the domain manually. It sucks. :frowning:
@Brian could you explain what strange things might happen in the context of some other real monads such as option/maybe and a reactive pattern monad. What does strange look like to a developer running across a problem. On Apr 11, 2013 7:46 PM, "Brian McKenna" notifications@github.com wrote:
@erights https://github.com/erights but Promises/A+ already provides a compatible then method. They are close to monadic. It's a problem with promises if they do strange things with other monads. They shouldn't.
— Reply to this email directly or view it on GitHubhttps://github.com/promises-aplus/promises-spec/issues/94#issuecomment-16250312 .
@killdream By "stripped version of structural typing", if you mean, "duck typing", yes. And yes, it is sad. There are many sadder things that we live with because of historical constraints.
"every superset of { then :: Function } is considered a Promise =/". No. every superset of { then :: Function } is considered a thenable. That's an important difference.
"every superset of { then :: Function } is considered a Promise =/". No. every superset of { then :: Function } is considered a thenable. That's an important difference.
Is there somewhere that describes exactly what semantics a Thenable is expected to have? I've taken to calling monads in Javascript "Thenables", and I'd like to know if I'm reading into it too much.
@erights well, it doesn't make much of a difference in this case, as it's more of an issue with them always being treated as something really close to a Promise in semantics, such that you can safely assimilate them.
It should be pretty obvious that interop with anything but promises is 100% out of scope for the promises spec and the promises community.
People still wanting examples:
// I'd implement this differently but I'd have to explain
function Optional(a) {
this.hasValue = typeof a != 'undefined';
this.value = a;
}
Optional.of = function(a) {
return new Optional(a);
};
Optional.none = new Optional();
Optional.prototype.then = function(f) {
if(!this.hasValue) return this;
return f(this.value);
};
Let's write a single function to add my last name to both promises and optional values (assuming promises have constructor.of
):
// Helper library
function map(x, f) {
return x.then(function(a) {
return x.constructor.of(f(a));
});
}
function addLastName(m) {
return map(m, function(name) {
return name + ' McKenna';
});
}
Promises in application.js:
addLastName(promisedNmae)
.then(function(name) {
// Brian McKenna
console.log(name);
});
Optional values in test.js:
addLastName(Optional.of("Brian"))
.then(function(name) {
// Brian McKenna
console.log(name);
});
@pufuwozu Thanks for all the time and attention you have given to providing this argument. My points were not to say /A+ promises were close enough, but rather that it seems that an A+ promise can be wrapped into a monadic value as you describe it. And in that sense, it may well be that the spec is "good enough" for both communities, the promises community that is focused on what seems a more intuitive (and powerful) interface focused on a particularized use case.
I think the point made by @Twisol is that the /A+ promises functionality of a two-parameter then cannot be identically reflected from a pure monadic promises implementation, but the pure monadic implementation can be built from the /A+ specification. This suggests to me that the status quo can be resolved simply with /A+, and a monadic wrapper surrounding it. Clearly, promises folk can't rely on libraries depending on monads without wrapping, but once wrapping they can have all the benefits, with the corresponding costs.
Where am I dropping the thread here?
I think the point made by @Twisol is that the /A+ promises functionality of a two-parameter then cannot be identically reflected from a pure monadic promises implementation
Just to clarify: The presence of a more powerful function doesn't mean that it's somehow not a "pure" monad. The promise wrapper you mention could easily expose a 2-ary then
-like function; it's just outside the scope of what a monad requires. (See Haskell's Either and its either
function, they're pretty directly analogous.)
@erights it's interesting that polotek/procstreams has been brought up a few times when people start talking about promises/monads, etc. I haven't been following all of the debate. I don't have that much of a vested interest. But since you've addressed me directly, I'll respond here for future reference.
Asking me to "fix my library" presumes that you've decided it's broken. It also presumes that I would agree with you that it's broken. My library works as designed for the most part. It's even got some tests on it (non-exhaustive as they may be). The point of contention here seems to be my use of the names "promise" and "then" in my design. Before I respond to that, let me say I actually really like the work done here with promises A+. Not because I subscribe to all of the ideas here. But because I love to see folks working together to improve their corner of the world. It's great work, and when/if I ever pick up a promise implementation, I'd take a second to check if it was A+ compliant. Because I think you all have put a lot of thought into things.
That said. I think it's a bit disingenuous to suggest to me and everyone else who ever uses the names "promise" and "then" that they should conform to A+. Even considering that I agree with what you're trying to accomplish here. There are several reasons for this. One is the presumption that I share your goal of interoperability. "until @polotek fixes his library, it can't co-exist with various promise libraries". Procstreams weren't designed to co-exist with promise libraries. No one has asked for it. I'm not sure how it has somehow become a requirement for your design, or how subsequently fixing that is my problem. Or maybe I'm misunderstanding your point. Are you saying that the promises A+ spec cannot and should not try to accomodate all Thenables
in js? Because that I agree with.
I appreciate folks here for thinking of me. But procstreams probably won't change. Not because I'm a jerk. But because I like the way it is now. And more importantly, no one here has bothered establishing a connection with me that would make me inclined to accomodate you. If the plan is to effectively convert all Thenables in all libraries to support A+, this may come up a lot. Pulling people into random arguments and telling them to fix their code probably isn't the best tactic.
@polotek "Pulling people into random arguments and telling them to fix their code probably isn't the best tactic."
Hi Marco, you are correct. I apologize. I really like your clarification and agree with much of what you have to say.
@tonymorris while @domenic's words weren't the best ones throughout this thread, I don't think we need any more fighting here.
@domenic Is there an accepted way to banish someone from further posts? I admin some email lists. After a post like @tonymorris's, I would banish that person, probably forever. But the mechanics of discussion on Github are new to me.
@erights I'm pretty sure there's no built in way to do so. It would probably be worth e-mailing GitHub support though, they may well help.
Brian Mckenna criticised current spec. He proposes to use FP approach to achieve much better modularity.
Suggest to read it, really good ideas with just three changes.
http://brianmckenna.org/blog/category_theory_promisesaplus
His proposal is to incorporate into spec three simple apis:
Promise.of(a)
will turn anything into promise.Promise#then(f)
should take one function, not two.Promise#onRejected(f)
: moveonRejected
to prototype instead of second arg.edit: see https://github.com/promises-aplus/resolvers-spec/issues/24 for more discussion