Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

point function #24

Closed
Raynos opened this Issue · 124 comments
@Raynos

@pufuwozu brought up a good point with his article

http://brianmckenna.org/blog/category_theory_promisesaplus

The notion of having a point function that takes a value and returns a promise would allow for writing powerful higher order functions.

Basically prior art, a-la category theory shows that having a function that looks like point is a good thing, we should consider this.

@juandopazo juandopazo referenced this issue in promises-aplus/promises-spec
Closed

Incorporate monads and category theory #94

@puffnfresh

Specifically, the point function needs to take ANY value and create a fulfilled promise containing that value.

  • The function should be easily accessed, preferably on the constructor property of a promise value.
  • It can't have a special case for promises. Nested promises NEED to be possible or it will break a heap of laws, not allow deriving useful functions and break refactoring.
  • It can't take a callback to generate a value. The argument should be be the fulfilled value.

It's a very, very simple function but when combined with then, promises will form a monad. Being a monad allows us to write functions which work the same for many structures. It allows DRY code.

@juandopazo said there was possibly a proposal for a from method. Sounds good to me.

Promise.prototype.constructor = Promise;
Promise.from = function(value) {
    // Implementation of fulfilled promise that contains value
    // ...
};
@ForbesLindesay
Collaborator

I'm very keen for something broadly of this nature. I really want a way to take a promise, and create a completely new promise of the same library (i.e. with the same extension methods and helpers). Almost any time I'd use this though, I'd actually wan the behavior I've suggested elsewhere in other issues which I've called "assimilate" thus far, which essentially just looks like:

Promise.assimilate = function (valueOrThenable) {
  return new Promise(function (resolve) { resolve(valueOrThenable); });
};

This would be considerably more convenient for the use cases I have in mind, but by the sounds of things wouldn't help your use case at all. It would always be fulfilled with a value that was not a thenable/promise (or be rejected if the thenable/promise was rejected).

Could you try and explain why we need a way to create nested promises? Preferably in terms that don't require much knowledge of category theory, as I (and probably many others reading these posts) don't have vastly more knowledge of it than what you mentioned in your blog post.

@puffnfresh

@ForbesLindesay it's surprisingly very, very simple. The first reason is a concept called parametricity. Basically, given a function like the following:

point :: a -> Promise a

We can't tell on the inside of the implementing function what the structure of a actually is. Of course in JavaScript we can rely on duck-checking, tag-checking or something similar but that breaks the parametricity law. Breaking the law might be useful in some cases but now other laws will break!

So immediately, the next law I see breaking is the "left identity" monad law. It says these two things should always be equal:

  • Promise.from(a).then(f)
  • f(a)

What happens when a is a promise? If from "flattens" it then f will get the fulfilled content of a! Definitely not the same as just calling f(a).

Why are laws useful? Couple of reasons:

  • Lets us derive functions which we know will work for all monads, functors, applicatives, etc.
  • Lets us say which refactorings are possible (e.g. 1 + 2 can be rewritten as 2 + 1 because of the commutativity law)

Quoting "What happens to you if you break the monad laws?":

In short, "breaking the monad laws" should generally be read as "writing buggy code".

@Raynos

Breaking the monad laws is like the difference between a promise and a jQuery deferred. i.e. subtle bugs and having to write special cases :(

@ForbesLindesay
Collaborator

@pufuwozu thanks, that was a nice explanation.

@Raynos doing something that bad would indeed be terrible.

I still think what's needed now is an example of something practical (that could be seen in a typical application) that would benefit from the monad behavior over non-monadic behavior. I think the difficulty you'll have persuading people to accept this is that most people still view promises as representations of future values (or errors) rather than as a special case of monads.

Coming from that it's difficult to see why those cases matter. I don't see anything else in JavaScript (e.g. an array) behaving like a Monad in a way that lets me write a function that's clear and simple to reads and does something useful to both a promise (with the point extension) and an array (presumably with a similar point extension).

@techtangents

I think the difficulty you'll have persuading people to accept this is that most people still view promises as representations of future values (or errors) rather than as a special case of monads.

Well, they're both. Monad is an interface that a promise implements. As a result of implementing this interface, a large set of functions become available. It's a tremendous amount of free code for the cost of implementing 2 methods.

You'll find that a large set of types are monads. Combine this with the large amount of operations that apply to all Monads, and you can see that Monad is an incredibly powerful abstraction.

Lots of things are Monads and each one of them gets a stack of functions for free. What's there not to love?

It also comes at no detriment to your code. I have yet to see a single feature of the promise specs that would break as a result of implementing Monad. It's win-win.

I don't see anything else in JavaScript (e.g. an array) behaving like a Monad in a way that lets me write a function that's clear and simple to reads and does something useful to both a promise (with the point extension) and an array (presumably with a similar point extension).

Note that Monad is an abstraction, so the operations that arise from Monad are probably more abstract than you're used to. As they apply to different data types with different flatMap/point implementations, they will mean different things to different types - but the differences are encapsulated in the flatMap/point implementation - the abstract function just deals with those functions. In essence, Monad is a pattern - you demonstrate how your type matches that pattern, then the Monad operations deal with the pattern itself.

I'll give you an example: flatten.

If I have a Promise of a Promise of an x, I can flatten that to get a Promise of an x.
If I have an Array of Arrays of x, I can flatten that to get an Array of x.
If I have an Option of Option of x, I can flatten that to get an Option of x.
... repeat for all Monads... and there are a lot of them

All with one function. I don't have to write flatten for each type, I just write it once for Monad and it just works for all Monads.

Now, Monad is just one of a set of type classes / interfaces that are generally useful. Other common ones are Functor and Applicative. All Monads are Applicatives and all Applicatives are Functors. Same as Monad, if you implement these other type classes, you gain a library of functions for free.

More things are Functors than Monads, but Monad gives you more operations for free. That's why it's one we really like. Note the function "liftA2" that Brian mentioned above - the 'A' stands for Applicative. So, the type doesn't need to be a Monad to use this function - it only has to be an Applicative, which is slightly less restrictive and has some nice properties that Monad doesn't have.

And this is really just dipping your toes in the water. Open your minds to these new abstractions and a whole new world of awesome opens up right before your eyes.

@techtangents

BTW - here's a future library I developed ages ago: https://github.com/techtangents/jsasync

It uses "Futures" and "Asyncs" - I'm not sure if my terminology is in line with everyone else, though. Anyway, its future is a value that might be available in the future. Its Asyncs are a wrapper around functions that return a future.

In this library, I've implemented Functor, Applicative and Monad for Future, and Category and Arrow for Async. Category and Arrow are two other very useful abstractions that abstract things like functions.

If you've come across function composition, you've encountered Categories. Composition of 2 functions generalises to composition of two Categories.

@puffnfresh

Concrete example:

// Constructor
function Person(name, age, postcode) {
    this.name = name;
    this.age = age;
    this.postcode = postcode;
}

var me = lift3(Person, name, age, postcode);

The lift3 will give us a Person only when all three fields "have values".

In our application code, let's pretend that name, age and postcode are promises:

name = httpGet('name');
age = httpGet('age');
postcode = httpGet('postcode');

But we don't want to make HTTP requests in our unit tests! Let's make them "optional values" to see if the logic still works:

name = someValue('Brian');
age = someValue(23);

postcode = noValue;
run();
assert(me == noValue);

postcode = someValue(4017);
run();
assert(me.get().postcode = 4017);

Both promises and optional values are monads with similar semantics. The me value only relies on the monad class so we can make it asynchronous or synchronous just by changing the monad (i.e. the wrapper around name, age and postcode)!

@ForbesLindesay
Collaborator

OK, thanks, this has been really useful and informative. What I'd like to propose we do to move things forwards is:

  1. Make sure we don't prohibit having a method to create a promise for a promise in any of the specs (I still don't fully understand the implications of doing so, but I'll take your word for the fact that it's important).
  2. Create a separate spec for how "Monadic Promises" should work. This would include the point, flatMap and onRejected operations, with exactly the semantics desirable for them to be monads.

This spec needs to include a duck typing test for "Monadic Support" (I'm not sure if "monadic" is the right word for "like a monad"). It will be optional, in the sense that not all promise libraries will support it, but if it proves useful, many will support it. You can then write libraries that require their promises implement the "Monadic Promises" spec. They can just either wrap promises that don't implement "Monadic Support" or throw a TypeError.

This will keep the core APIs for consuming and creating promises simple for people who have approached the problem from a software development background (e.g. callbacks) rather than a category theory background, because they can continue to use the simple to understand .then method and the constructor proposed here. But it will allow libraries to support more advanced functionality.

@ForbesLindesay
Collaborator

If @paulmillr, @Raynos, @pufuwozu, @techtangents and the obvious key individuals in the promises-aplus organisation are happy with that as a way to proceed, someone can create another repository for this monad spec.

Additionally, I wonder whether the Category Theory/Monads world has anything interesting to say about cancellation or progress, as both specs are still very much a work in progress and they're thorny issues which are not yet well understood via being implemented & used.

@paulmillr

+1 for from method. ECMAScript 6 specifies Array.from and maybe Set.from.

@bergus

-1 for using from as in ES6 Array.from. It is more like our assimilate procedure, and I would expect Promise.from to take any thenable and transform it into a valid promise.

Instead, I'd opt for Promise.of as the point/return function whose point is to wrap a value into a Promise for it. This resembles Array.of, and I think it is also more descriptive than "from".

@puffnfresh

@paulmillr wow, thanks for that. I took a look at the spec - from is not what we want, we want of!

http://people.mozilla.org/~jorendorff/es6-draft.html#sec-15.4.3.4

> Array.of(1)
[ 1 ]

I propose we name this method Promise.of - it will work very similar to Array.of.

@techtangents

+1
As an aside: Google Guava also uses 'of' in constructing their data structures. e.g. ImmutableList.of(3).

@juandopazo

So immediately, the next law I see breaking is the "left identity" monad law. It says these two things should always be equal:

  • Promise.from(a).then(f)
  • f(a)

We've run into this issue when discussing the behavior of resolver.fulfill. The problem is with the overloaded then. Take the identity function:

function identity(x) {
  return x;
}

One would expect that promise.then(identity).then(identity) would return a promise for the same value as the first promise. However, if point lets you create a promise of a promise, then

Promise.point(somePromise).then(identity).then(function (value) {
  assert(value === somePromise); // false
});
@ForbesLindesay
Collaborator

One of many reasons that a "promise for a promise" is undesirable. When @domenic said you need to start by writing code, I think he was correct. Begin by writing something that does Promises/A+ but with your extensions. https://github.com/nathan7/pledge/blob/master/pledge.js is about the most minimal implementation I've seen, so would be a good starting point. build your promise monad, make it still pass the promises/A+ test wuite (shouldn't be too difficult). Build a cool (and open source) real-world application that uses it, refine it as needed, then lets talk about incorporating the good bits into currently active promise libraries.

I'm voting to close this issue now as I don't think point or of is going to make it into v1 of the resolvers-spec.

@techtangents

I think this thread just took a turn for the worse.

Can you please explain what's going on here?

One would expect that promise.then(identity).then(identity) would return a promise for the same value as the first promise. However, if point lets you create a promise of a promise, then

Promise.point(somePromise).then(identity).then(function (value) {
assert(value === somePromise); // false
});

@techtangents

We've run into this issue when discussing the behavior of resolver.fulfill. The problem is with the overloaded 'then'

Perhaps this is the problem. Having functions that do different things depending on the type is a bad idea. It impacts your ability to reason about what the function does, and breaks the parametricity law.

Looking at http://brianmckenna.org/blog/category_theory_promisesaplus, it looks like you're trying to use 'then' to be map and flatmap - this is a bad idea. They're completely different concepts. For a promise, flatMap chains the result of an asynchronous computation into another asynchronouse computation, returning a Promise of the combined result. Map, on the other hand, just runs a (synchronous) function over the output. Very different use cases.

If you're writing a spec to be used by multiple implementations, you can't afford to make this sort of mistake. It just has "bug" written all over it.

Let me be clear: parametricity lets you reason about a type-parameterised function independently of the type being passed in. i.e. you can reason about what effect map or flatMap independently of what type of value is being passed in. This is important everywhere, but has particular importance for a dynamically-typed language.

@techtangents

Let me use an array analogy.

map for arrays maps a function over each element in an array.
e.g. map([1, 2, 3], function(x) { return x + 1; }); returns [2, 3, 4];

flatMap for arrays maps a function that returns another array over the array, then collapses the result.
e.g. flatMap([1, 2, 3], function(x) { return [x, String(x)]; }; returns [1, "1", 2, "2", 3, "3"];

Now, what if I were to write this:
map([1, 2, 3], function(x) { return [x, String(x)]; };
I would expect this to return:
[[1, "1"], [2, "2"], [3, "3"]]

Note that map and flatMap of the same function over the same array return different values.

However, if I only had a "then" function that chose map and flatMap, one of those behaviors would be impossible to write.

@juandopazo

Yes, that's the issue with promises for promises. And IMHO it's unfortunately too late to fix it. There is a lot of code out there that depends on this being "broken".

@techtangents

Well, isn't it just the "then" function that has this mixed map/flatMap behavior? Sorry if this has already been asked, but could you just add map and flatMap? "then" behaves as-is, and map and flatMap behave like the Functor and Monad interfaces require.

Would this be a workable solution? Old code keeps working and you get to implement Monad.

@puffnfresh

I think I've been pretty good with giving examples of where things will break. Can someone do the same with of?

I don't know the reason why it can't be properly implemented. Thanks.

@ForbesLindesay
Collaborator

It can't be implemented if you require promises for promises using the existing then behavior. That isn't going to change because promises for promises don't actually make sense as a concept. It makes sense for some monads, but not for promises. As such, it's not going to change.

I'd suggest you create an orthogonal spec that specifies the behavior of a flatMap method, but don't overload then if you want anyone to actually use it.

@puffnfresh

Promises of promises do make sense as a concept. I've used them in other implementations.

To be a monad, it has to allow nesting (like I showed above), otherwise it is not a monad.

What I think you're saying is that Promises/A+ can not define a monad because it can not define an of function. I am trying to ask "why?"

I have pointed at laws and given example code whenever I say something won't work. Can someone please do something similar?

If it is a problem with Promises/A+ then that is sadly a missed opportunity :(

People are already using Fantasy Land but thanks for the feedback.

@juandopazo

I have pointed at laws and given example code whenever I say something won't work. Can someone please do something similar?

I did. See above: #24 (comment)

@puffnfresh

@juandopazo ah thanks, forgot to reply to that.

Functor laws as specified in Fantasy Land:

  • u.map(function(a) { return a; })) equals u
  • u.map(function(x) { return f(g(x)); }) equals u.map(g).map(f)

It's really great to see that you want to preserve the first Functor law! In a language with types, it's proven to always be true so we don't even have to worry about it (due to parametricity). How cool is that!?

Anyway, if we were able to of a promise to a promise and get a nested promise then it would trigger then's overloaded functionality and it wouldn't behave like map :disappointed:

But then we satisfy the monad laws and can derive lots of useful methods. For example, a map that satisfies the functor laws!

function map(p, f) {
    return p.then(function(a) {
        return p.constructor.of(f(a));
    });
}

So you see that you're saying that having no of means that we can have a map that works for any function that doesn't return a promise. I'm saying that having an of means we can implement a map that works for any function that returns anything!

More equivalence laws the better. Laws allow us to derive useful functionality and easily reason about what code will do what. You've said that we want the functor laws. Great. Why don't we want to add one function to satisfy the monad laws as well?

@juandopazo

That sounds nice, but it's sadly breaking expectations one way or another. With the overloaded then we either break the left identity law or we break the first functor law. I'd say we're in a pickle.

@puffnfresh

@juandopazo so if of is doesn't work for promise values, then there will be broken laws. If of does work for promise values, it's possible to work around and create methods that don't have broken laws.

I really, really want a non-broken of so I can work around it - that's a much better pickle.

@juandopazo

@pufuwozu how would you work around the broken functor law?

@briancavalier briancavalier referenced this issue in promises-aplus/promises-spec
Closed

Investigate Promises/A+ compatibility w/a Monad system #97

@ForbesLindesay
Collaborator

The reason a promise for a promise doesn't work is because a promise isn't just a mathematical concept. It represents something concrete:

A promise represents a value that may not be available yet.

It isn't really a thing in and of itself, it's just a representation of a thing we don't have yet. Therefore if is fulfilled, we should have a value. It simply doesn't make sense for a value that may not be available yet to become available and still be a value that may not be available yet.

The same logic applies fairly well to option types, but not to arrays. I personally don't see that much practical use for sharing code between promises and arrays. The only one that I see making obvious sense is flattening, but our promises always remain as flattened as possible.

I'd love to see an example of a monad, which makes sense being nested, that can usefully share code that also works for promise monads.

@puffnfresh

@juandopazo the map function that I defined above satisfies both functor laws when given a working of:

function map(p, f) {
    return p.then(function(a) {
        return p.constructor.of(f(a));
    });
}
@puffnfresh

@ForbesLindesay promises can be a mathematical concept. It's called a monad.

How about the identity monad as an example?

function Id(a) {
    this.value = a;
}
Id.of = function(a) {
    return new Id(a);
};
Id.prototype.then = function(f) {
    return f(this.value);
};
@puffnfresh

Taken from work: we represent everything as a future value but switch to the identity monad in tests (so that they're deterministic). Extremely useful.

@ForbesLindesay
Collaborator

No, I get that the identity momand exists, it's not an example, unless you have some code that:

  1. works for both the identity monad and a promise monad
  2. breaks if the promise monad doesn't support nesting
  3. can't be done in an alternative way that's much clearer

Yep, I often do a similar thing in tests. I represent all asynchronous results as promises, then switch to a promise that's already resolved in tests. In general that's done by writing new Promise(function (resolve) { resolve(value); }), but many libraries provide a helper (e.g. Q provides Q(value)).

I have no nested promises, so I have no need to have nested monads in my tests, I just use promises that are already resolved.

@ForbesLindesay
Collaborator

Nested Identity monads don't really make a whole lot of sense either. The only example I've seen so far that does is the Array, and although I can see that some code could be shared between promises and arrays, they're really pretty different beasts, and probably better off that way.

@puffnfresh

@ForbesLindesay we do have code that works in the identity monad and a promise monad. It would break if the promise monad didn't support nesting. It can't be done an alternative way - we rely on the monad abstraction.

I'm not lying about this maths stuff being a useful abstraction. We use it and things break when they're not done properly.

The identity monad has nothing to do with arrays. What's that about?

@ForbesLindesay
Collaborator

Show me an example of such code. I've given you 3 points as requirements, I think they're fair, and I think I could meet equivalent requirements for two promise libraries and any current feature in the promises spec.

In your example, you might also want to try and answer the following 3 questions

  • Why would you need to nest an identity monad?
  • Why would you need to nest a promise monad?
  • What real world concept would such nesting represent?

I look forward to reading your example.

(as for the point about arrays, you've mentioned before that arrays are monads, they can have map/flatMap methods, and it makes sense to nest them, but it doesn't make sense to nest any other monad I've come across)

@juandopazo

@pufuwozu right, map keeps returning a promise for a promise, so map(map(promise, identity), identity) still works. However, the problem I see is discoverability of the issue by unsuspecting developers. If we add of allowing it to wrap promises, they may run into the broken functor law issue when using then and it's pretty hard to figure out. Keep in mind that the identity function is the default behavior for then when the first parameter is undefined.

However, if we don't introduce an of that lets you wrap promises in promises, you could still work arround it by writing your own of function, as long as there is a way to create a promise for a promise, even if it's not convenient, am I right?

@puffnfresh

@ForbesLindesay my answer is to 1, 2 and 3 is because that's a requirement to satisfy the left identity monad law. If the law is not satisfied, there will be bugs.

Now, you ask, what type of bugs? Now, that's hard. When algebraic laws are broken, subtle things happen. Classic example of associativity of addition of numbers (semigroup):

// Should print the same thing twice. Rule of associativity of addition.
function add(a, b, c) {
    console.log((a + b) + c);
    console.log(a + (b + c));
}

add(1234.567, 45.67834, 0.0004);
// 1280.2457399999998
// 1280.24574

That's subtle detail that you don't usually think about when dealing with floating point numbers. With strings it always works:

add("one", "two", "three");
// onetwothree
// onetwothree

You have to be careful whenever you use addition! Some things look like they support it, but they actually support a broken version.

Because these laws are in some sense "part of" the type class, it should be reasonable for other code to expect they will hold, and act accordingly. Misbehaving instances may thus violate assumptions made by client code's logic, resulting in bugs, the blame for which is properly placed at the instance, not the code using it.

The monad laws involve a bit more code to break - but when they'll break, that'll make it even more subtle:

In short, "breaking the monad laws" should generally be read as "writing buggy code".

I'm going to attempt an example - but this doesn't show up with just one line of code unless you point at join. It's a combination of code that results in bugs.

@puffnfresh

@juandopazo if you specify an of that does something special when Promises are involved, it's not possible to write a correct of from the specification.

@ForbesLindesay
Collaborator

So far I've seen "Appeal to Authority" and the analogy of associativity of addition. I knew all that about addition, but we've come to terms with the fact that Q(v).then(fn) is not the same as Q(fn(v)). Nor should it be. Other than that one, I haven't seen any other examples of seemingly obvious rules being broken.

Still eagerly awaiting actual code replacing appeal to authority.

@puffnfresh

@ForbesLindesay "Nor should it be" - yes it should, according to the law! The authority is an algebraic law. Come on.

I'll try to find an example but do you really think that algebraic laws are not important?

@psnively

@ForbesLindesay What an odd complaint! We accept the authority of mathematical logic in order to be able to explain existing applications of the logic and predict the results of future applications of the logic. This is part and parcel of what it means to program—by the Curry-Howard Isomorphism, if you want yet another "appeal to authority"—and it's a very poor reflection on the JavaScript community to place actual appeals to authority, i.e. existing code, culture, tradition, aesthetic judgment, etc. etc. etc. literally ad nauseam, before actual reasoning.

@juandopazo

@pufuwozu yeah I wasn't very clear. What I meant is that I wouldn't want to provide a short form that could lead to bugs with then, because most developers will jump to use the short form. But I am thinking maybe we should add a more obscure way to do it so you could still write your own point function.

For example, the DOMFuture way of creating promises is:

var promise = new Future(function (resolver) {
})

Where:

  • resolver.accept lets you fulfill the promise with any value, including promises
  • resolver.resolve takes any value, but if the value is a promise, it is assimilated
  • resolver.reject rejects the promise with a reason

In that case you could monkey patch Future and add the methods you need:

Future.point = function (value) {
  return new Future(function (resolver) {
    resolver.accept(value);
  });
};
Future.prototype.map = function (fn) {
  var promise = this;
  return promise.then(function (value) {
    return promise.constructor.point(fn(value));
  });
};

...and you'd have a nice monad-compatible implementation.

@puffnfresh

@juandopazo I'm happy for a solution. I'm not happy that making things buggy for the sake of making buggy things less buggy in a certain situation.

I think making a non-broken of is the right thing to do. The behaviour of then is a problem. That should stay a problem with then and not lead to introducing problems across the specification.

@ForbesLindesay
Collaborator

@pufuwozu @psnively these are not appeals to algebra. You haven't presented a mathematical proof. You are applying the semantics you believe should exist. If promises were monads, then the semantics you provide would be correct, but they're not. As such, the current semantics are equally valid and useful. The current semantics allow me to take an object which is promise-like but perhaps not a promises/A+ promise and convert it to a real promise. Yours allow you to satisfy your desired aesthetics, which you have chosen to label as algebra.

If you can present a mathematical proof that Q(v).then(fn) should be equivalent to Q(fn(v)) without making arbitrary assumptions as to the semantics of Q and then I will of course back down. As it stands, the current semantics are easy to reason about and useful in real world programs.

@juandopazo

@pufuwozu I think you're thinking buggy in the context of the code you want to write. But most people using promises will just be interacting with then, not building other abstractions on top of it. In fact, most peple already have problems understanding promises and how to use them. That's why I think we shouldn't standardize a short form that could lead them to bugs with then, but we should leave the door open for you to be able to write a useful map.

@copumpkin

I think the point is that, regardless of whether @ForbesLindesay says that nested promises "don't make sense", the fact remains that you could easily implement them to work the way @pufuwozu says.

Monad is an interface, Promise is a computational concept that exists widely outside of javascript or this particular hypothetical implementation. Promise can easily be made an instance of Monad, and has been in many other implementations. Whether you want that or not is another question, but none of the reasons ("appeal to lack of imagination" it appears) stated so far for not doing it seem particularly convincing. The fact that the lack of imagination extends so far as to saying that nested option or identity types don't make sense, you'd think you'd just look up examples of why nesting is a desirable property (and is fundamental to monad-ness) rather than just talking about how it doesn't make sense. People have been talking about this for literally decades.

@ForbesLindesay
Collaborator

I continue to wait for a real example of code. Contrary to what you have been saying, these are eminently not provable. You can make decisions about the semantics of various methods and then prove things about what behavior certain programs have. You could also make assumptions about the behavior of certain programs and work backwards to prove what the semantics of various methods must be. You can't prove what the semantics of both should be, because it's not a mathematical absolute, it's a trade-off of pros and cons.

@puffnfresh

@ForbesLindesay promises are monadic in lots of implementations for lots of languages. Monadic implementations must satisfy those laws or things are broken. Saying that promises are not monadic only applies to this specification - not to promises themselves.

I'm arguing that the promises specification is very close to being a monad and thus would have to obey those rules. Your argument against making it a monad is what? That it's useful to not be a monad?

It's much more useful to be monad - breaking the law for an arbitrary reason is not useful.

@robotlolita

As it stands, the current semantics are easy to reason about and useful in real world programs.

Having implemented a Promises/A+ compliant library, and used it in real-world scenarios, I think I can provide my own experience on this: not true. You can't reason about what:

map(makeDirectory, lines(readFile('foo.txt')))

Will do without knowing how each of those functions handle the values passed to them wrt promises. It looks obvious what the code should accomplish, but it's not obvious what it, in fact, accomplishes. The problem gets worse if readFile or lines don't return a proper Promises/A+, or if you add even more functions to the mix.

@puffnfresh

@ForbesLindesay in sane promise libraries it is mathematical. That's what I want.

Why don't you want that?

@ForbesLindesay
Collaborator

They are mathematical, in the sense that everything is, but so are ours. You're saying Promises/A+ are wrong because they're not mathematical, monads are correct because they are mathematical. That argument simply doesn't hold water unless you have a definition of mathematical that's very different to the standard, accepted definition.

@ForbesLindesay
Collaborator

Nobody has mathematically proved monads are the best way of doing things. Or, by extension, that promises are worse than monads as a way of doing things. So far you haven't provided a single example in which monads would be superior to promises.

@puffnfresh

@ForbesLindesay being a monad means I can abstract over them. Like I said before, I do this all the time at work. It's extremely useful. I also have functions that work on promises and work on other things.

It's great.

Why should we not allow that?

@robotlolita

@ForbesLindesay the only argument I see @pufuwozu is that Promises in this specification are close to being a Monad. Being a Monad is useful because you get lots of abstraction for free (the same way being a Functor is useful).

Edit: For example. You can write one library that deals just with Arrays, one that deals just with Vectors, one that deals just with Lists, one that deals just with Sets. And reimplement the same operations for all of them over and over again. Or you can write operations that are based on the fact that all of these share some similarities, so your operations should work neatly with all of them (and all other types to be). I fail to see how this isn't a compelling reason.

@puffnfresh

They're close to being a monad. Being a monad would allow me to abstract over them and write lots of useful functions that also work for other structures.

Why do we not want to allow that?

@ForbesLindesay
Collaborator

No, you have functions that work on promises, and work identity objects. And since a promise is a superset of an identity object, just use a promise to represent your identity object.

You get just as much useful abstraction from being a promise as you get from being a monad.

@killdream we've demonstrated time and time again that they are not close enough to being a monad to make the transition easy (possible?) without breaking existing code.

@copumpkin

Take replicateM, sequence (fine, that works with Applicative, but the point still stands), or filterM. I can envision use cases for those on promises, and they all involve "nested promises".

Why not keep the funky existing behavior on then and make new methods that satisfy the laws? Existing code won't break, and people who want to use generic monad operations get to use them anyway.

@ForbesLindesay
Collaborator

If you wish to continue to be taken seriously I'd suggest you stop writing "allow me to abstract over them" and start writing "allow me to abstract over them differently". You can abstract over promises. You can represent an option as a promise, you can represent a future as a promise, you can represent an identity as a promise.

@copumpkin

He's talking about abstracting over monads, not promises. The operations I listed in my previous comment work over values of any monad, and make perfect sense to use on promises too.

@Peaker

@ForbesLindesay No, the abstract functions @pufuwozu is talking about aren't just for promises and identity objects. They are also for lists, option types, parsers, exceptions, probability distributions, etc.

If you do what @pufuwozu is requesting, then he could use a nice API in the same way with promises as he does with his probability distribution code, or his parsers. If you reject it, you gain nothing -- but you lose this, meaning that you're going to have to duplicate all of your code for each of the above cases.

@puffnfresh

@ForbesLindesay no, a promise is not a superset of an identity object.

Can I abstract over state as a promise? Can I abstract over reader as a promise? Can I abstract over writer as a promise? Can I abstract over IO as a promise?

Promises lack the laws necessary to allow the above. The laws are important - we can't use promise as a better monad. It's less general.

@ForbesLindesay
Collaborator

@copumpkin

I don't know the functionality of the methods you list, and they have far from self-explanatory names.

The reason for not just creating a new method, is that avoiding the creation of promises for promises is widely seen as a useful goal, so we may not want to allow any method for doing so. You still need to demonstrate that doing so is necessary.

I know he was speaking about monads. The point is he was comparing monads to promises. I can abstract over promises, so his assertion should just be that he can abstract differently over monads.

@puffnfresh

@ForbesLindesay monads are more general than promises. Does that answer your question?

@puffnfresh

@ForbesLindesay self-explanatory names aren't going to be easy when we're dealing with abstract algebra, right? :smile:

@copumpkin

@ForbesLindesay then take two minutes to look up what they do! The goal here is to improve the spec, and it's not like @pufuwozu is intentionally trolling you because he wants you to fail. If a dozen people seem to think there's something worth noting here, don't you think the obvious reaction of someone who wants to improve their spec should be to go learn about that thing, rather than to set up fortifications and stand their ground? @pufuwozu clearly cares enough to keep trying to drive the point home, but it isn't his responsibility to teach you. If your goal is genuinely to get the best spec, just go read about what we're talking about. You might find it's a load of bullshit, which is fine (I don't think it's bullshit, obviously), but give it an honest try and then come back and ask questions rather than making assertions or demanding that we prove to you that our points are worth your consideration before you take any sort of initiative to take them seriously.

The goal here isn't to win the argument.

Edit: I dare you to implement the list monad, the continuation monad, a probability density monad, the search monad, or parser combinators as promises.

@ForbesLindesay
Collaborator

Here's my ID implementation that's a promise:

function ID(value) {
  return new Promise(function (resolve) { resolve(value); });
}

and here's an option type:

function Option(hasValue, value) {
  return new Promise(function (resolve, reject) { if (hasValue) { resolve(value) } else { reject() });
}

I can now write functions which abstract over Promises, and they will work fine abstracting over IDs and Options too.

@ForbesLindesay
Collaborator

I continue to wait for an example that demonstrates the advantage.

@psnively

@ForbesLindesay One reason that you aren't getting examples is exactly as @copumpkin explained; these examples are well-rehearsed elsewhere (yes, including other Promise implementations) and we aren't going to do your homework for you. If you want to claim authority over this JavaScript spec, fine, but if you want to be taken seriously, it behooves you to gain a little more education rather than stomping your feet like a petulant child and insisting that it's your way or the highway.

Hint: the fact that there's existing code with bad abstractions over which you can write N more bad abstractions one by one isn't a counterexample.

@ForbesLindesay
Collaborator

No, my goal isn't to win an argument, it's to get a spec that's best for the future of software writing in JavaScript.

If I were attempting to persuade the makers of the HTML specification that we needed a new smell tag, that browsers would implement by generating gasses with the appropriate scent (when hardware support was available), it would be up to me to prove to them that it was worthwhile. Not up to them to learn about the uses of smells on the internet and in flash applications.

If I wanted to add a blink tag to GitHub flavoured markdown, it would be up to me to persuade GitHub that lots of people wanted to display blinking text in their issues and readmes. Not up to them to go looking for people who wanted that feature.

You want to add a feature, and thus complexity, to a well established spec, that's in use widely across the web, both client and server. As such, it's up to you to demonstrate that your feature adds enough value to be worth the added complexity. Not up to me to try and prove it adds value. I'm happy with what we already have. I'm not attempting to prove anything and I'd be really happy if you showed me something awesome I'm not yet aware of that we could do if only promises were monads. I'd then implement them in promise, rejoice at the new functionality I got then, after using it in a project, come back here and tell everyone how wonderful it was so that we could put it in the spec and implement it in other promise libraries.

@joneshf

@ForbesLindesay I'm not sure if you got a decent example of a nested monad (and to be honest, I started skimming the last 20 or so messages), but there's always trees.

@Peaker

@ForbesLindesay When you implement the Identity Monad and Option Monads with Promise result and Promise return, you actually implemented CPS'd Identity and Option. Identity/Option should lend themselves to case analysis, without needing to block on a promise.

Also, these are 2 monads that you can approximate with Promises. How do you implement the State/Reader/Writer monads? Or the List monad? Or the continuation monad?

@copumpkin

Oh, I'd misunderstood the purpose of this exercise, then. I thought the goal was to get a better spec, not just to describe what was already in place. I also thought the overarching goal for developers/computer scientists in general was to learn new things, so the passive "prove to me that this is worth my time" (when there are literally hundreds of articles all over the internet about this) is a little surprising. It's not a smell tag and it's not a blink tag. It's basic mathematics that underpins/describes a lot of what happens in your day-to-day experience, whether you realize it or not.

I'm just disappointed by your lack of curiosity as an engineer more than anything. Don't tell me that the function names aren't descriptive; JFGI. In fact, that pretty much applies to this entire conversation, since our side of the conversation is pretty much standardized across the internet.

@ForbesLindesay
Collaborator

@joneshf no, I never did. Trees seem like a nice obvious example, so lets run with that.

So, a tree of promises wouldn't actually require nesting promises at all:

function PromiseTreeNode(value, left, right) {
  return new Promise(function (resolve) {
    resolve( { left: left, right: right, value: value} );
  });
}
var tree = PromiseTreeNode('root', PromiseTreeNode('left'), PromiseTreeNode('right'));
@Peaker

@copumpkin @pufuwozu Why not paste a few monadic combinators, with a couple of refactorings that depend on the monad laws, a couple of short programs that utilize the combinators, and then show how these programs could be used with promises or parsers/monad-of-the-day, but that won't be possible due to the refusal to have nested promises?

Sure, this is a boring homework assignment, but it seems to me that relative to the amount of time you've already invested in this, it should be a breeze :)

@Peaker

Also, to @ForbesLindesay 's defense, the standard material on this stuff is not approachable if you don't have a background in any of Haskell, Agda, Coq, CT, etc. So transliterating a tiny subset of the required learning material to Javascript to illustrate would probably be helpful.

@copumpkin

Yeah, I apologize for making overly broad critical statements about @ForbesLindesay there. That kind of attitude just rubs me the wrong way, to the point of getting nasty myself.

The fact remains that I don't have time to keep participating in this discussion, and don't think it really should be my duty to teach about this (although I do clearly enjoy teaching it on IRC, as I'm sure you've observed). Perhaps @pufuwozu will take the time, but that's his prerogative and would be very nice of him :) I'm also not sure that a bug tracker is really the best place to teach things like this.

@dustinjuliano

@ForbesLindesay The Monad laws provide combinatorial expressiveness and a common binding that are "well-behaved" with an extremely large class of operations. This just makes sense from a language level; promises entail nonlinear changes of state in a way that can be delivered linearly in syntax. And monads have been used to deliver that kind of referential encapsulation in many languages. The most important reason to give @pufuwozu a listen is that promises could be improved without taking anything away from them conceptually. The onus shouldn't be on him when in fact there are dozens, if not hundreds, of written materials on the use and benefits of these abstractions in functional languages. One need not be a Haskell dev to realize that greater generality and a proven interface/pattern for combinatorial expression will make promises even better.

@ForbesLindesay
Collaborator

@Peaker thanks, that's exactly what I'm asking for

@copumpkin It's not a lack of curiosity, it's the fact that there's much more to learn than I'll ever manage to learn in one lifetime. I'm in my final year of university so I have finals to prepare for. I care deeply about the next version of JavaScript, so I'd like to learn all about the different proposals for that (I created http://esdiscuss.org to help make that close to possible). I'm building a continuous integration system of JavaScript on the client side. I'm not asking you to prove I should continue learning stuff, I'm asking you to prove I should learn this, instead of something else.

@joneshf

@ForbesLindesay sorry, I didn't actually mean a tree of promises, I just meant that trees are monads and trees can be nested, so that was supposed to be an example of why you would want to recursive monads.

@ForbesLindesay
Collaborator

OK, so why does that translate to needing nested promises? Other than a flatten function, what else would you do with a promise that would require it to be nestable?

@Twisol

Lets say I have a function query that takes a string of user input, sends it to the server, and returns a promise for the server's response. Now, I want to get the user's input, so I put up a popup window and create a promise userInput that represents the user's input.

userInput.then(query) :: Promise (Promise Response)

This type is quite exquisite. It explicitly shows that there are two different threads of execution involved here - three if you include the present thread of execution - and we have access to either one. In fact, we have access to the response thread from within the userInput thread, which is something we lack if Promises are pre-flattened.

There's one big reason why I think this is superior: it forces you to acknowledge and explicitly deal with the nesting if you want to access the response promise. This makes it clear that the response thread is entirely "later" than the userInput thread, so you don't have to keep track of a bunch of timeline interrelations yourself. Refactoring becomes easier, because you can immediately identify which parts of the code affect which thread of execution.

As for an actual bug in one, or outright feature in the other, that's a lot more subtle. A major benefit of these laws, as has been mentioned, is that they aid the programmer in equational reasoning (which directly affects reading and refactoring). There's certainly no reason why you can't have a helper method that follows a .then() immediately by a .flatten(). In fact, this helper method is still a different method from then, and serves to call out the "later-ness" just as explicitly.

@Raynos Raynos referenced this issue in briancavalier/avow
Closed

Implement `Promise.of` #2

@ForbesLindesay
Collaborator

OK, I can see how it makes that more explicit. My impression of it though is that in JavaScript it just creates more re-factoring hazards and bugs because it forces me to always know whether something is a promise or a value. For most of the code I write, that would make things much less DRY.

Results of Some Research

I've done some research, just to keep everyone happy. I've spent the last hour watching http://www.youtube.com/watch?v=dkZFtimgAcM and also took the time to take a couple of notes here.

@puffnfresh

@ForbesLindesay oh no, don't watch Crockford's talk. He gets a lot of things wrong.

@puffnfresh

@ForbesLindesay your notes are fair. It is hard to imagine a case where a nested promise will break. I'll eventually come up with a small example that breaks horribly.

Sadly, I'm going to have to spend some time on it, just to show you something I know will break.

@Twisol

Following on @pufuwozu's point, I recommend this one by Brian Beckman.

it forces me to always know whether something is a promise or a value

For what it's worth, I usually track the types in my Javascript code anyway. I put a docu-comment containing a type signature before functions that need it, or I tag a line that needs more clarification. And I've taken to the convention of suffixing promise variables with a $, like so:

user$ = server.getUser("Twisol");
user$.then(function(user) {
  // ...
});

For most of the code I write, that would make things much less DRY.

I'm not sure I follow that one. If you define a helper method that just calls p.then(f, g).flatten(), you have exactly the same code you had before (but with a different name instead of then). If I'm missing something here, can you clarify?

@bergus

This thread seems to be still on the wrong track. Just to remember, the original assignment was to get a point function on Promises :-) So I'm gonna comment on some older post…

@juandopazo:

The problem [to fulfil left idendity] is with the overloaded then

I don't see that. To me, it's a problem of an overloaded constructor. In fact, we will need two of them: One that assimilates thenables etc, and one that follows point semantics.

Indeed, Promise.of(a).then(f) is equivalent to f(a) (assuming f is returning a promise - if not, it would at least behave the same while having different return values) and so satisfies the law.

One would expect that promise.then(identity).then(identity) would return a promise for the same value as the first promise.

No. Or only if you expect then to act like a Functors' fmap - which it does not always due to overloading. If you had explicitly used the map function as given above by @pufuwozu and later by @juandopazo, it would work:

promise.map(identity) ≡ promise

But if you are expecting the monadic then, it will fulfill the right identity law:

promise.then(Promise.of) ≡ promise
@ForbesLindesay
Collaborator

@Twisol

If it doesn't flatten automatically that requires writing additional code. This would either need to be added everywhere, or I'd have to remember to add it whenever re-factoring caused it to be needed. Besides, then is not going to change that dramatically. It's not too late to change it to only unwrap one layer of promises. i.e. if your callback returns a promise for a promise, it only unwraps the first promise, not the second one. It's far too late for a .then that doesn't unwrap at all, not that anyone who's using promises actually wants that.

@bergus

I agree this went off topic. The issue is that although I'm not dead set against having a .of that supports returning a promise for a promise, I don't really see the point of it. I just don't see the use case. I've tried hundreds of phrasings of that question, but nobody's actually answered the question of "why are promises for promises necessary?"

@puffnfresh

@ForbesLindesay "why are promises for promises necessary?" <- to not break the monad laws. You have an answer. What you want is an example, of which I have one in my head. I'll write it down sometime soon.

@briancavalier

nobody's actually answered the question of "why are promises for promises necessary?"

From my perspective, I feel like answers that have been given, have tended to be "because otherwise we break Monad laws". Unfortunately, that is only a helpful answer to someone who already understands what Monad laws are and why they are important. To someone who doesn't have that experience/perspective, it doesn't really help. What would likely help is some further explanation, examples, or at the very least some links for further reading.

@pufuwozu your answer was serendipitously timed. It appeared as I was typing the above paragraph.

I understand everyone is busy, but if you could indeed take the time to write down that example, I think it would be hugely beneficial to moving forward.

@techtangents

nobody's actually answered the question of "why are promises for promises necessary?"

This has been answered many times over. Let me summarise:
1. Identity law
2. Parametricity
3. Practical example:
#24 (comment)

Parametricity is a very important concept that I'd like to highlight.

Parametricity means that when you create a data type that's parameterised over another type, it works the same regardless of the type parameter. e.g. if I have a list of strings, it behaves exactly like a list of integers, for all list operations. This means also that your data type implementation deals only with the structure and function of that data type itself. That structure and function then applies identically across all instances.

The logical expression is of this form:
For all values of a type X there is a type Y(X).
e.g. for all types X, there is a type Array of X.

(In JavaScript, the type X is really the 'universal' type as references are untyped)

This principle is deemed so important that many languages enforce it. e.g. in Java, if I am implementing a new structure FunkyList, then in my class definition, I cannot call any methods of values of type A, because I don't know anything about their type is (unless I do reflection, but that defeats the point).

So, the benefits of parametricity are thus:
1. As a user of a data type Y(X), I can be assured that Y behaves identically for all type parameters X.
2. As the creator of data type Y, parametericity means that I don't have to test Y for any specific values of X - if I can test that it works for a single type parameter, it will work for all.

It follows, then, that I should be able to create a Promise of Promises, because I can create a Promise of anything! There is nothing special about a Promise that prevents it from being a type parameter of Promise.

Also, if I were to create a library of useful functions on top of Promises, I wouldn't know what type of Promise was being passed in. However, in order to reason about what my functions do, I need to be able to reason about what the base Promise functions do. If the base functions behave one way for all types of values, my job is easier.

it forces me to always know whether something is a promise or a value
Well, a promise IS a value. More generally, this is a complaint about "I don't know what type this value is", which is a fundamental problem with dynamic typing and not specific to this discussion.

Let me take a different approach. A Promise is a computation which may produce a value in the future. A function is a similar concept - it just produces a value when it is invoked. So, a Promise is very similar to a function (with its arguments applied), and these ideas generalise.

More specifically, a Promise is a type of value - the asynchronous equivalent of Id. A Promise may produce a value in the future; an Id is just a data cell that can produce a value right now.
A function that returns a Promise can be considered an asynchronous function - the asynchronous equivalent of a function.

So, we're using these concepts just to model a different execution mechanism and different data channel for returning results. So, they generalise.

Ok, so a Promise of a Promise reads thus: I wish to have an asynchronous computation which produces another asynchronous computation.

Applying the the general concept here is thus: I wish to have a computation which produces another computation. This sounds familiar - when applied to functions, we call this Higher Order Functions. Currying is a typical example of this. It is well-established that Higher Order Functions and Currying are useful.

So... a Promise of a Promise can be thought of as a curried Promise!

So, what of this generality?

Looking at Promise vs Id. What concept abstracts the notion of "a value produced by a specific means or in a specific structure"? Well, we call them Functor, Applicative and Monad.

Looking at "function" vs "asynchronous function (function that produces a Promise)". Well, these concepts generalise to Arrow and Category. (Given that Promise is a monad, a function that produces a monadic value is an abstraction known as a Kleisli. All Kleislis are Arrows and all Arrows are Categories).

In our case, the whole point of the Promise data type is to encapsulate what it means for a value to be produced in the future. We are encapsulating this behavior into a data type. "Produced in the future" is just one mechanism that a value may be produced - we can abstract on this mechanism, and that is what Monad is useful for.

I can produce a value one way, or another, or a third way - it doesn't matter. I can define functions that operate identically regardless of how the value is produced, just by encapsulating "how that value is produced" in a Monad instance.

So, why is this useful? As has been explained, a large number of types are Monads and Monad gives rise to a lot of operations. i.e. DRY.

Ok, let's look at "point". Aside from being required for Monad and Applicative, it is practically useful for a few reasons.

And, well, some practical examples?
I do a HTTP request to get a URL for another HTTP request. I may want to do the first request now, and the other later. This is a Promise (Promise HttpResponse). I evaluate the first one, which gets me the Promise I need to use later on to get my final data.

Sure, you could just store the URL. But, what if it isn't always a URL? What if the server sometimes gave a URL of an image, sometimes base64-encoded data of an image? Then I have a Promise (Promise Image).

Say I wish to poll a URL. You could do this with recursive setTimeout. SetTimeout is a way of constructing an asynchronous computation. So, a setTimeout loop polling a URL is an infinite nesting of Promise (Promise (...)). This can also be represented as a stream or signal - this is basically what Functional Reactive Programming is.

Then, lets go to testing. Generally, in testing, you wish to not perform side effects because:

  • they're slow
  • they impact the real world
  • they may require a browser
  • you want to run them on a CI server in a console on Node

So, what can we do? There are a few techniques:

  1. Instead of constructing a Promise that returns a value from a HTTP request, let's just create a Promise that returns a pre-set value. By parametricity, these should behave identically. So, I can test my code by using "point" aka "of", instead of a side-effecting one.

  2. This still leaves us with an asynchronous computation, which can be messy to deal with in tests. So, if we abstract over the mechanism of computation, we can test with values of a more convenient computation (e.g. Id) and still gain the same confidence in our correctness.

So, to summarise:
1. We want to implement Monad, because it is incredibly useful.
2. We want to obey parametricity, because it makes it easier to reason about for the implementer and caller, and avoids run-type type inspection. Also, because it is required for the laws of Monad, Applicative and Functor. Parametricity is of particular importance in a dynamically-typed language, as you never know what type you're going to get passed in.
3. We want to do these things to make asynchronous computations behave like synchronous computations and many other computations in general.
4. Use cases of nested Promises have been demonstrated. You have a nested Promise whenever you have an asynchronous computation that produces some value that needs to be input to another asynchronous computation.
5. Abstracting over a mechanism of computation is useful in order to reason about your code abstractly and has practical benefits in testing.
6. You're creating a standard for promises, so it may be useful to consider the common aspects of promise implementations in other languages. Implementing Monad is certainly common and has been demonstrated in JS promise libraries.
7. Honestly, the liftA2 function @pufuwozu showed should be more than enough on its own to demonstrate the benefits of his proposal.

I really think the benefits of @pufuwozu's proposals are irrefutable and any remaining reluctance just comes out of fear and a need for some learning. Rest assured, though, there are a lot of resources out there to help, and people who will volunteer their time to mentor anyone with an open mind and a willingness to learn.

@techtangents

I think we need to close this thread. The benefits have been explained many times, and I believe no convincing counter-argument has been proposed.

Beyond this, it's up to the project contributors to either accept or reject the proposals.

I sincerely hope that you see the benefits of what has been proposed. Know that the wider programming community is watching this thread, and it has become a bit embarrasing for us as the JS community that we are actively refuting some very well-established computer science, mainly out of attitude, fear and unwillingness to learn.

Moreover, it is an opportunity for us as the JS community to step up and say: yes, we are real programmers, we're not babies, we can take on board useful computer science concepts and wield them like professionals.

@ForbesLindesay
Collaborator

Give me a minute to finish reading your post, then we can discuss closing/not closing :smile:

@ForbesLindesay
Collaborator

Right, I've read that, and taken my time to digest it. This explanation was good, it was clear, and I think I'm now in favor of us moving in this direction, or at least not ruling it out yet.

liftA2 wasn't good enough, because I still don't know what it does, it's really not obvious. Code that's not obvious rarely wins arguments.

I think it would be well worth you re-posting that comment somewhere as a Gist so we can link to it from other discussions around this issue as "motivation for promises for promises". I think it would also benefit greatly from a few proper section headings.

Moving Forwards

I want to see consensus, here's my proposal for how I think this can be achieved:

.then

It is probably not too late to change the behavior of recursively flattening promises at the end of a .then callback into just flattening one layer of promises. I will open an issue to this effect, and attempt to explain the reasons why I have shifted my view on the issue. This would make .then equivalent to fantasy land's chain.

It should be noted that other than that change, you won't succeed in affecting any other changes to the semantics of then. As such, people should refrain from any discussions relating to strict typing, separating error handling, functions that return pure values (i.e. map) etc. etc.

.map

Map can be implemented on top of then and of if we get it to flatten a single layer rather than multiple layers. As such, we should stop talking about it and no further mention of it should occur in the promises-A+ world until after all other points have been met. At such a point, the request would need to be for a separate specification for map. I suspect that you won't ever get this specification integrated into proimses-aplus, but that's OK because you can still implement map in terms of .then and .constructor.of.

.constructor.of

This won't be added directly to the objects themselves, that would be very weird, some objects may support it as a convenience function.

You may succeed in getting this added to the constructor. This will almost certainly be in a separate spec, not the resolvers spec, and not the promises spec. This is low priority, providing that no spec explicitly states: "There must not be a way of creating a promise for a promise". As such, you should defer all discussion on this matter until after the discussions of .then are concluded.

This issue

Since we are now looking for a different named method to the one you started out asking for and a lot of space has been taken up debating the rights and wrongs of promises for promises, I propose we close this issue. I think this issue should be renamed to Importance of Promises for Promises. I think a summary should then be added that should look like:

# EDIT: Summary of Discussion

Promises for promises were discussed at great length.  The discussion was concluded with the arguments in favor of allowing promises for promises summarized in [this gist](link to gist containing @techtangents latest post with headings etc) and the arguments against are summarized in [this gist](I'll create a gist, or someone else can).

I think once the discussions surrounding then are concluded, I think a new issue should be opened on this repository requesting the creation of a repository in which to discuss the possible specification of the .constructor.of method.

If this gains @Raynos's approval, he can make the necessary adjustments to this issue, alternatively so can someone who is an admin/collaborator on this repository.

@puffnfresh

Here's an example of where a broken of will break a program.

I write a small module in terms of an abstract monad. In production I have everything asynchronous. In testing I have everything blocking for determinism (woo, easy deterministic tests!).

In my code, I use the great lift4 function. The lift4 function takes a 4-arity function and then 4 monadic values as arguments - it then returns a monadic result.

lift4(f, a, b, c, d)

Let's make this more concrete. f will become a function that creates a user on a server (i.e. it returns a monadic value). Its arguments are name, password, age, address.

var userMM = lift4(createUser, nameM, passwordM, ageM, addressM);

(Where the M suffix means a monadic value and MM means a monadic value of a monadic value, i.e. the values are in a promise or some other monad, depending on context)

Under the identity monad we get an Identity(Identity(User)). Under broken promises we get Promise(User). That's strange but let's keep going.

Now, let's say the returned userMM has an id property from our database. I have an existing function called getId:

// Takes a monadic user, gives back the monadic user ID.
function getId(userM) {
    return userM.map(function(user) { return user.id; });
}

Now, I can just do this:

var keyM = userMM.chain(getId);

It'd work for actual monads but not promises. The ReadWriteState monad would call chain, get the monadic user and then map the monadic user into a monadic id. Promises would call chain, get the actual user value and then getId would call map on the user! The wrong thing!

@ForbesLindesay are you happy with that for an example? It's both concrete and realistic.

@puffnfresh

Well, glad you were convinced one minute before my example :smile:

@puffnfresh

@ForbesLindesay the Fantasy Land specification allows of to be on the value or the values constructor. Either one. Code abstracting over it must fall back to either.

@ForbesLindesay
Collaborator

OK, to be clear, I'm happy that this needs further consideration, and more eyes on it from the promises side of the fence, rather than just me + a few people in neither camp + everyone from fantasy land (I still think that's a terrible name for a spec you want people to actually use).

Just a point of clarification: Why can't you fix the above by just making Identity also do the same recursive flattening?

@ForbesLindesay
Collaborator

P.S. Link to my summary gist: https://gist.github.com/ForbesLindesay/5392612

The corresponding arguments for will need to be a bit longer and include examples as those arguments are less well understood by the promises community.

@techtangents

Map can be implemented on top of then and of if we get it to flatten a single layer rather than multiple layers.

Say what?

map shouldn't do any flattening. Map should take a Promise and a function and return a Promise that runs the function over the result of the original Promise. Just like how map over an array works.

"map" comes from the Functor type. The Functor laws state that map should return a value of the same structure. So, take a Promise of x, run a function x -> y over it and get a Promise of y.

One way of thinking of "flatMap" is that it maps then flattens.

As I understand it, this bizarre "then" function would be written in terms of map and flatMap.

@puffnfresh

@ForbesLindesay as for limiting Identity of Identity. How far does it go? There's actual use cases for Optional Optional values. Or Either Either values. It also ruins parametricity, as @techtangents pointed out.

@techtangents

And it breaks the Monad laws.

@puffnfresh

@ForbesLindesay just s/Identity/Optional/g in my comment. You need to represent Optional Optional values.

@ForbesLindesay
Collaborator
function map(entity, fn) {
  if (typeof entity.map === 'function') {
    return entity.map(fn);
  } else if (entity.chain === 'function') {
    return entity.chain(function (val) { return entity.constructor.of(fn(val)); });
  } else if (entity.then === 'function') {// && if then only flattens one layer, not multiple
    return entity.then(function (val) { return entity.constructor.of(fn(val)); });
  }
}
@ForbesLindesay
Collaborator

In answer to @techtangents' "say what?"

@ForbesLindesay
Collaborator

I still find the idea of an optional optional a bit weird, it would be hugely helpful if someone had an example, but I'll accept it for now.

@techtangents

That's really weird.

Return a promise which evaluates the original promise, but maps a function over it.

function map(promise, fn) {
  return promise(function(x) {
    callback(fn(x))
  });
}
@techtangents

I still find the idea of an optional optional a bit weird, it would be hugely helpful if someone had an example, but I'll accept it for now.

Well, you can think of an Optional as a List of either zero or 1 elements. Now think of a List of Lists.

@charleso

I still find the idea of an optional optional a bit weird, it would be hugely helpful if someone had an example

James Iry has an example of that:

You're writing a cache system (in the form of an associative map) in front of a persistent storage with nullable values. Question: how do you differentiate between "I haven't cached this value" from "I've cached the value and it was null in the database"? The answer is that you can't use "null" to mean both things, it's ambiguous.

@ForbesLindesay
Collaborator

Excellent example, thanks @charleso .

@techtangents I don't get what you're not happy with, it's even mentioned as something you can do in the Fantasy Land Spec.

@ForbesLindesay
Collaborator

@Raynos just created https://gist.github.com/ForbesLindesay/5392701 I want to gather two separate gists and make each as strong as I can. I'll start incorporating other examples/counter arguments into each.

@techtangents

@techtangents I don't get what you're not happy with, it's even mentioned as something you can do in the Fantasy Land Spec.

I must have missed something. Is this a way to provide a map implementation for things that have either map, chain or then, as an interoperability thing?

@ForbesLindesay
Collaborator

Yep, it aims to implement map for anything which has either map, (chain and of) or (then and of but where then behaves like chain and only flattens one level).

P.S. if you put a new line after the quoted bit, it won't render the whole thing as a quote, it would also be helpful if you could go back and fix the formatting of a few past posts.

@ForbesLindesay
Collaborator

@Raynos I've attempted to incorporate your example into the gist so I've deleted your comment as I want to keep them both free of commentary. I've included your arguments (rephrased slightly) in https://gist.github.com/ForbesLindesay/5392701 There may be more to add, if so, please make suggestions in comments on that gist not the gist giving arguments against promises for promises.

@ForbesLindesay
Collaborator

How do people feel about my previously proposed suggestion for ultimately closing this issue. I feel the discussion has gone on too long with too much back and forth repetition in this issue to expect newcomers to catch up. This is why I want to start a fresh with just a summary of the arguments for and against that were discussed in this issue.

@Twisol

@ForbesLindesay: Sounds like a good proposal to me. I'm glad to hear about your moment of epiphany!

@Raynos

@ForbesLindesay what we really need is two new issues in the correct place. One with an amendum to .then() to not recursively flatten. The other is an amendum to whatever to add .of() (this probably requires speccing out the semantics of of() in more detail).

@ForbesLindesay
Collaborator

I'm happy to see that issue for of. I just think that, since we're very close to releasing a new promise spec that would forever make this a backwards incompatible change to the spec, we should focus on that for the time being.

If you ask for one, small change, and justify it, it's much more likely to get proper consideration than if you open lots of simultaneous issues asking to change everything. Lets first argue for the value behind only unwrapping a single layer, because if we have that, we can move forwards without breaking backwards compatibility. To this end, I have opened promises-aplus/promises-spec#101 which I think should serve as the only place for discussion on this issue for the time being.

If we can agree there that we want promises for promises to be a possible thing, you're very unlikely to see any arguments regarding promise.of being specc'd in a very simple, sane, way.

@techtangents

Also, @ForbesLindesay, you said you didn't understand liftA2. I've tried to explain this in http://www.techtangents.com/explanation-of-lifta2-in-javascript/

There are lots of other tutorials on Functors, Applicatives and Monads out there. Most of them are in Haskell or Scala, though, not JavaScript. I would highly recommend you learn Haskell as it makes it easier to understand these concepts, so you can then apply them to your coding in other languages.

@briancavalier

There's a ton of good information in this thread. Thanks, everyone.

I agree with @ForbesLindesay that it has reached the point where it's best to close and direct energy toward promises-aplus/promises-spec#101, specifically noting the process that has been set forth there.

@vendethiel vendethiel referenced this issue in folktale/data.task
Closed

Why do you think Promises A+ are broken? #6

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.