Re: The Paradox of Partial Parametricity

classic Classic list List threaded Threaded
51 messages Options
123
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Mark S. Miller-2
[+es-discuss]

I didn't realize that I composed this in reply to a message only on public-script-coord. Further discussion should occur only on es-discuss. Sorry for the confusion.


On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <[hidden email]> wrote:
I think the key exchange in these threads was

On Fri, May 3, 2013 at 4:17 PM, Jonas Sicking <[hidden email]> wrote:
[...]
I.e. permitting nested promises creates a more complex model and with
that you always get more confusion and more questions.

 On Sat, May 4, 2013 at 1:48 AM, Claus Reinke <[hidden email]> wrote:
[...]
From the perspective of a-promise-is-just-like-other-wrapper-classes,
auto-flattening promises creates a more complex model


Together with each of their explanations about what they meant. They are both right. Lifting and auto-lifting do not mix. Q Promises give us autolifting with no lifting. Monadic promises would give us lifting but no autolifting. Having both in one system creates a mess which will lead programmers into the particular pattern of bugs Jonas warns about in his message.

For clarity I define the following APIs so I can define the three architectural choices in terms of the subset of these APIs they contain. Obviously, I do not intend to start a bikeshed yet on the particular names chosen for these operations. Let's stay focused on semantics, not terminology.

An upper case type variable, e.g. T, is fully parametric. It may be a promise or non-promise.
A lower case type variable, e.g. t, is constrained to be a non-promise. If you wish to think in conventional type terms, consider Any the top type immediately split into Promise and non-promise. Thus type parameter t is implicitly constrained to be a subtype of non-promise. 




Ref<T> is the union type of T and Promise<T>.

Q.fulfill(T) -> Promise<T> // the unconditional lift operator

Q(Ref<t>) -> Promise<t> // the autolift operator

p.map: Promise<T> -> (T -> U) -> Promise<U>

p.flatMap: Promise<T> -> (T -> Promise<U>) -> Promise<U>
// If the onSuccess function returns a non-promise, this would throw an Error, 
// so this type description remains accurate for the cases which succeed.

p.then: Promise<T> -> (T -> Ref<u>) -> Promise<u>
// Ignoring the onFailure callback



* A Monadic promise system would have Q.fulfill, p.map, and p.flatMap.

* A Q-like promise system would have Q, and p.then

* The dominant non-Q-like proposal being debated in these threads has Q.fulfill, Q, and p.then. 




This note explains why I believe the last is much worse than either of the other two choices. As Jonas points out, generic code, to be useful in this mixed system, has to be careful and reliable about how much it wraps or unwraps the payloads it handles generically. Had programmers been armed with .map and .flatMap, they could succeed reliably. Arming them only with .then will lead to the astray. As an example, let's start with a variant of Sam's async table abstraction. The parts in angle brackets or appearing as type declarations only documents what we imagine the programmer may be thinking, to be erased to get the real code they wrote. In this abstraction, only promises for keys are provided. The get operation immediately returns a promise for the value that will have been set. (An interesting variant is a table that works even when the get arrives first. But we can ignore this wrinkle here.)


    class AsyncTable<T,U> {
        constructor() {
            this.m = Map<T,U>(); // encapsulation doesn't matter for this example
        }
        set(keyP :Promise<T>, val :U) {
            keyP.then(key => { this.m.set(key, val) });
        }
        get(keyP :Promise<T>) :Promise<U> {
            return keyP.then(key => this.m.get(key));
        }
    }

When U is a non-promise, the code above works as intended. The .then in the .get method was written to act as .map. When tested with U's that are not promises, it works fine. But when U is actually Promise<V>, the .get method above returns Promise<V> rather than Promise<U>. As far as the author is concerned, the .then is functioning as a broken .map in this case, with the signature

p.then: Promise<T> -> (T -> U) -> Promise<V>  // WTF?


This same abstraction in a monadic promise system would be written

    class AsyncTable<T,U> {
        constructor() {
            this.m = Map<T,U>(); // encapsulation doesn't matter for this example
        }
        set(keyP :Promise<T>, val :U) {
            keyP.map(key => { this.m.set(key, val) });
        }
        get(keyP :Promise<T>) :Promise<U> {
            return keyP.map(key => this.m.get(key));
        }
    }

This works reliably.


This same abstraction in a Q-like promise system would be written

    class AsyncTable<t,u> {
        constructor() {
            this.m = Map<t,u>(); // encapsulation doesn't matter for this example
        }
        set(keyP :Promise<t>, val :u) {
            keyP.then(key => { this.m.set(key, val) });
        }
        get(keyP :Promise<t>) :Promise<u> {
            return keyP.then(key => this.m.get(key));
        }
    }

This works reliably. After erasure, note that this is exactly the same as the first example. The difference is that the code now correctly implements the annotated types representing the programmer's intention.

In a system with autolifting, you can't get full parametricity of promises simply by adding an unconditional lift. You have to remove, or at least strongly discourage, autolifting. Or you have to take pains to carefully work around it.

The main failure mode of standards bodies is to resolve a conflict by adding the union of the advocated features. Here, this works even worse than it usually does. The coherence of lifting depends on the absence of autolifting, and vice versa. We need to make a choice.


--
    Cheers,
    --MarkM



--
    Cheers,
    --MarkM

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Mark S. Miller-2
On Fri, May 10, 2013 at 5:55 AM, Mark S. Miller <[hidden email]> wrote:
[...]
This same abstraction in a Q-like promise system would be written

    class AsyncTable<t,u> {
        constructor() {
            this.m = Map<t,u>(); // encapsulation doesn't matter for this example
        }
        set(keyP :Promise<t>, val :u) {
            keyP.then(key => { this.m.set(key, val) });
        }
        get(keyP :Promise<t>) :Promise<u> {
            return keyP.then(key => this.m.get(key));
        }
    }


Actually, it's even better: 


     class AsyncTable<t,u> {
         constructor() {
             this.m = Map<t,u>(); // encapsulation doesn't matter for this example
         }
         set(keyP :Promise<t>, val :Ref<u>) {
             keyP.then(key => { this.m.set(key, val) });
         }
         get(keyP :Promise<t>) :Promise<u> {
             return keyP.then(key => this.m.get(key));
         }
     }


--
    Cheers,
    --MarkM

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Mark S. Miller-2



On Fri, May 10, 2013 at 6:05 AM, Mark S. Miller <[hidden email]> wrote:
On Fri, May 10, 2013 at 5:55 AM, Mark S. Miller <[hidden email]> wrote:
[...]
This same abstraction in a Q-like promise system would be written

    class AsyncTable<t,u> {
        constructor() {
            this.m = Map<t,u>(); // encapsulation doesn't matter for this example
        }
        set(keyP :Promise<t>, val :u) {
            keyP.then(key => { this.m.set(key, val) });
        }
        get(keyP :Promise<t>) :Promise<u> {
            return keyP.then(key => this.m.get(key));
        }
    }


Actually, it's even better: 


     class AsyncTable<t,u> {
         constructor() {
             this.m = Map<t,u>(); // encapsulation doesn't matter for this example
         }
         set(keyP :Promise<t>, val :Ref<u>) {
             keyP.then(key => { this.m.set(key, val) });
         }
         get(keyP :Promise<t>) :Promise<u> {
             return keyP.then(key => this.m.get(key));
         }
     }


The way one would actually write in Q is even better

      class AsyncTable<t,u> {
          constructor() {
              this.m = Map<t,u>(); // encapsulation doesn't matter for this example
          }
          set(keyP :Ref<t>, val :Ref<u>) {
              Q(keyP).then(key => { this.m.set(key, val) });
          }
          get(keyP :Ref<t>) :Promise<u> {
              return Q(keyP).then(key => this.m.get(key));
          }
      }


The difference is instructive. The Q version of Postel's law is to either require t or Ref<t>, and to either provide t or Promise<t>.


-- 
    Cheers,
    --MarkM

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Kevin Smith
In reply to this post by Mark S. Miller-2
 
    class AsyncTable<T,U> {
        constructor() {
            this.m = Map<T,U>(); // encapsulation doesn't matter for this example
        }
        set(keyP :Promise<T>, val :U) {
            keyP.then(key => { this.m.set(key, val) });
        }
        get(keyP :Promise<T>) :Promise<U> {
            return keyP.then(key => this.m.get(key));
        }
    }


The way to make this work would be to lift the value stored in the map.

    get(keyP :Promise<T>) :Promise<U> {
        return keyP.then(key => Q.fullfill(this.m.get(key)));
    }

Do you agree?  Is your premise that forgetting such a "lawyer-ly" detail will amount to a foot-gun?

{ Kevin }


_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Mark S. Miller-2



On Fri, May 10, 2013 at 6:58 AM, Kevin Smith <[hidden email]> wrote:
 
    class AsyncTable<T,U> {
        constructor() {
            this.m = Map<T,U>(); // encapsulation doesn't matter for this example
        }
        set(keyP :Promise<T>, val :U) {
            keyP.then(key => { this.m.set(key, val) });
        }
        get(keyP :Promise<T>) :Promise<U> {
            return keyP.then(key => this.m.get(key));
        }
    }


The way to make this work would be to lift the value stored in the map.

    get(keyP :Promise<T>) :Promise<U> {
        return keyP.then(key => Q.fullfill(this.m.get(key)));
    }

Do you agree?  Is your premise that forgetting such a "lawyer-ly" detail will amount to a foot-gun?

It's more than my premise, it is my point ;).

If you think of .then as a being approximately .flatMap, then you might think to do this manually. But given the behavior of .then, programmers will as often think of it as .map-like as they will think of it as .flatMap-like. If code like the above is needed to use .then reliably, then its .map-like behavior is only a distraction leading people into using a behavior they cannot use reliably. In a system with .fulfill, what purpose is served by giving .then this dual role?

 

{ Kevin }




--
    Cheers,
    --MarkM

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Claude Pache
In reply to this post by Mark S. Miller-2

Le 10 mai 2013 à 14:55, Mark S. Miller <[hidden email]> a écrit :

[+es-discuss]

I didn't realize that I composed this in reply to a message only on public-script-coord. Further discussion should occur only on es-discuss. Sorry for the confusion.


On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <[hidden email]> wrote:
I think the key exchange in these threads was

On Fri, May 3, 2013 at 4:17 PM, Jonas Sicking <[hidden email]> wrote:
[...]
I.e. permitting nested promises creates a more complex model and with
that you always get more confusion and more questions.

 On Sat, May 4, 2013 at 1:48 AM, Claus Reinke <[hidden email]> wrote:
[...]
From the perspective of a-promise-is-just-like-other-wrapper-classes,
auto-flattening promises creates a more complex model


Together with each of their explanations about what they meant. They are both right. Lifting and auto-lifting do not mix. Q Promises give us autolifting with no lifting. Monadic promises would give us lifting but no autolifting. Having both in one system creates a mess which will lead programmers into the particular pattern of bugs Jonas warns about in his message.

For clarity I define the following APIs so I can define the three architectural choices in terms of the subset of these APIs they contain. Obviously, I do not intend to start a bikeshed yet on the particular names chosen for these operations. Let's stay focused on semantics, not terminology.

An upper case type variable, e.g. T, is fully parametric. It may be a promise or non-promise.
A lower case type variable, e.g. t, is constrained to be a non-promise. If you wish to think in conventional type terms, consider Any the top type immediately split into Promise and non-promise. Thus type parameter t is implicitly constrained to be a subtype of non-promise. 




Ref<T> is the union type of T and Promise<T>.

Q.fulfill(T) -> Promise<T> // the unconditional lift operator

Q(Ref<t>) -> Promise<t> // the autolift operator

p.map: Promise<T> -> (T -> U) -> Promise<U>

p.flatMap: Promise<T> -> (T -> Promise<U>) -> Promise<U>
// If the onSuccess function returns a non-promise, this would throw an Error, 
// so this type description remains accurate for the cases which succeed.

p.then: Promise<T> -> (T -> Ref<u>) -> Promise<u>
// Ignoring the onFailure callback



* A Monadic promise system would have Q.fulfill, p.map, and p.flatMap.

* A Q-like promise system would have Q, and p.then

* The dominant non-Q-like proposal being debated in these threads has Q.fulfill, Q, and p.then. 



Hello,

With this explanation, from my perspective (as someone who has never used promises, but rejoices in advance to use them), the Q-like model of promise seems far superior to me:

* lifting (for non-promises) + no-op (for promises) is advantageously replaced by one method, autolifting (working with both);
* `flatMap` (for promises) + `map` (for non-promises) is advantageously replaced by one method, `then` (working with both);

It follows that:
* it is impossible to have a promise for a promise, reducing the probability of bugs;
* it makes it easier to write generic algorithms that work for both non-promises and promises;
* in many situations, there is no need to ask oneself if one should provide a promise or a non-promise, reducing the burden of thinking to programmers and therefore the probability of bugs.

Not wanting to ask oneself «Should I provide a promise or a value?» is not sloppiness, but it is because promises are just uninteresting wrappers. I never ask myself: «Should I provide a string or a String object to the `substring` method?», because it doesn't matter, and the String object is just an uninteresting wrapper. And to push the comparison further: it is dubious, and hopefully impossible, to wrap a String object in another String object, just like it is dubious to have a promise for a promise.




<snip>

The main failure mode of standards bodies is to resolve a conflict by adding the union of the advocated features. Here, this works even worse than it usually does. The coherence of lifting depends on the absence of autolifting, and vice versa. We need to make a choice.

Indeed, the advantages I have mentioned vanished if we add unconditional lifting to autolifting + `then`, because its bare use forces the programmer to think what type of object he should provide (a promise or a non-promise) in some situations. If you allow me to do a somewhat shaky comparison, the usefulness of ASI is greatly reduced by the few situations where ASI does not work; therefore most style guides recommend to not use ASI at all.


—Claude




--
    Cheers,
    --MarkM




_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: The Paradox of Partial Parametricity

Ron Buckton-2

Following Tab’s comments on the a Promise monad, I prototyped a Future library based on DOM Future using TypeScript. Since TS has no concept of a union type, I’m using TS overloads to approximate the Ref<T> union type example. It has roughly the following API:

 

```ts

class FutureResolver<T> {

  accept(value: T): void;

  resolve(value: Future<T>): void;

  resolve(value: T): void;

  reject(value: any): void;

}

 

class Future<T> {

  constructor(init: (resolver: FutureResolver<T>) => void);

 

  // unconditional lift

  static accept<TResult>(value: TResult): Future<TResult>;

 

  // autolift

  static resolve<TResult>(value: TResult): Future<TResult>;

  static resolve<TResult>(value: Future<TResult>): Future<TResult>;

 

  static reject<TResult>(value: any): Future<TResult>;

 

  // assimilation of thenables, similar to `Q()`. Assimilation stops at the first `Future`

  static from<TResult>(value: any): Future<TResult>;

 

  // autolifting then.

  // for `p.map` like operation, `resolve` should return `Future.accept(u)`

  // for `p.flatMap` like operation, `resolve` can return u or `Future.resolve(u)`, but no error is thrown

  then<TResult>(resolve: (value: T) => Future<TResult>, reject: (value: any) => Future<TResult>): Future<TResult>;

  then<TResult>(resolve: (value: T) => TResult, reject: (value: any) => Future<TResult>): Future<TResult>;

  then<TResult>(resolve: (value: T) => Future<TResult>, reject: (value: any) => TResult): Future<TResult>;

  then<TResult>(resolve: (value: T) => TResult, reject: (value: any) => TResult): Future<TResult>;

 

  catch<TResult>(reject: (value: any) => Future<TResult>): Future<TResult>;

  catch<TResult>(reject: (value: any) => TResult): Future<TResult>;

 

  done(resolve: (value: T) => void, reject: (value: any) => void);

}

```

 

In the AsyncTable example you would write:

 

```ts

class AsyncTable<T, U> {

  private m = new Map<T, U>();

 

  set(keyP: Future<T>, val: U): void {

    keyP.done(key => { this.m.set(key, val); });

  }

 

  get(keyP: Future<T>): Future<U> {

    return keyP.then(key => Future.accept(this.m.get(key))); // accept causes an unconditional lift.

  }

}

```

 

In Mark’s second example, using the union type, you might instead have:

 

```ts

class AsyncTable<T, U> {

  private m = new Map<T, Future<U>>();

 

  set(keyP: Future<T>, val: Future<U>): void;

  set(keyP: Future<T>, val: U): void;

  set(keyP: Future<T>, val: any): void {

    keyP.done(key => { this.m.set(key, Future.resolve(val)); }); // autolift `Ref<U>`-like union to `Future<U>`

  }

 

  get(keyP: Future<T>): Future<U> {

    return keyP.then(key => this.m.get(key)); // no need for unconditional lift, `then` will merge the already auto-lifted `Future<U>`

 }

}

```

 

And Mark’s third example might be:

 

```ts

class AsyncTable<T, U> {

  private m = new Map<T, Future<U>>();

 

  // gah! TS needs union types…

  set(keyP: Future<T>, val: Future<U>): void;

  set(keyP: T, val: Future<U>): void;

  set(keyP: Future<T>, val: U): void;

  set(keyP: T, val: U): void;

  set(keyP: any, val: any): void {

    Future.resolve(keyP).done(key => { this.m.set(key, Future.resolve(val)); });  // autolift key and val

  }

 

  get(keyP: Future<T>): Future<U>;

  get(keyP: T): Future<U>;

  get(keyP: any): Future<U> {

    return Future.resolve(keyP).then(key => this.m.get(key)); // autolift key, val is already a `Future<U>`

  }

}

```

 

Ron

 

From: [hidden email] [mailto:[hidden email]] On Behalf Of Claude Pache
Sent: Friday, May 10, 2013 11:25 AM
To: Mark S.Miller
Cc: [hidden email]; es-discuss
Subject: Re: The Paradox of Partial Parametricity

 

 

Le 10 mai 2013 à 14:55, Mark S. Miller <[hidden email]> a écrit :



[+es-discuss]

 

I didn't realize that I composed this in reply to a message only on public-script-coord. Further discussion should occur only on es-discuss. Sorry for the confusion.

 

On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <[hidden email]> wrote:

I think the key exchange in these threads was

 

On Fri, May 3, 2013 at 4:17 PM, Jonas Sicking <[hidden email]> wrote:

[...]

I.e. permitting nested promises creates a more complex model and with
that you always get more confusion and more questions.

 

 On Sat, May 4, 2013 at 1:48 AM, Claus Reinke <[hidden email]> wrote:

[...]

From the perspective of a-promise-is-just-like-other-wrapper-classes,
auto-flattening promises creates a more complex model

 

 

Together with each of their explanations about what they meant. They are both right. Lifting and auto-lifting do not mix. Q Promises give us autolifting with no lifting. Monadic promises would give us lifting but no autolifting. Having both in one system creates a mess which will lead programmers into the particular pattern of bugs Jonas warns about in his message.

 

For clarity I define the following APIs so I can define the three architectural choices in terms of the subset of these APIs they contain. Obviously, I do not intend to start a bikeshed yet on the particular names chosen for these operations. Let's stay focused on semantics, not terminology.

 

An upper case type variable, e.g. T, is fully parametric. It may be a promise or non-promise.

A lower case type variable, e.g. t, is constrained to be a non-promise. If you wish to think in conventional type terms, consider Any the top type immediately split into Promise and non-promise. Thus type parameter t is implicitly constrained to be a subtype of non-promise. 

 

 

 

 

Ref<T> is the union type of T and Promise<T>.

 

Q.fulfill(T) -> Promise<T> // the unconditional lift operator

 

Q(Ref<t>) -> Promise<t> // the autolift operator

 

p.map: Promise<T> -> (T -> U) -> Promise<U>

 

p.flatMap: Promise<T> -> (T -> Promise<U>) -> Promise<U>

// If the onSuccess function returns a non-promise, this would throw an Error, 

// so this type description remains accurate for the cases which succeed.

 

p.then: Promise<T> -> (T -> Ref<u>) -> Promise<u>

// Ignoring the onFailure callback

 

 

 

* A Monadic promise system would have Q.fulfill, p.map, and p.flatMap.

 

* A Q-like promise system would have Q, and p.then

 

* The dominant non-Q-like proposal being debated in these threads has Q.fulfill, Q, and p.then. 

 

 

 

Hello,

 

With this explanation, from my perspective (as someone who has never used promises, but rejoices in advance to use them), the Q-like model of promise seems far superior to me:

 

* lifting (for non-promises) + no-op (for promises) is advantageously replaced by one method, autolifting (working with both);

* `flatMap` (for promises) + `map` (for non-promises) is advantageously replaced by one method, `then` (working with both);

 

It follows that:

* it is impossible to have a promise for a promise, reducing the probability of bugs;

* it makes it easier to write generic algorithms that work for both non-promises and promises;

* in many situations, there is no need to ask oneself if one should provide a promise or a non-promise, reducing the burden of thinking to programmers and therefore the probability of bugs.

 

Not wanting to ask oneself «Should I provide a promise or a value?» is not sloppiness, but it is because promises are just uninteresting wrappers. I never ask myself: «Should I provide a string or a String object to the `substring` method?», because it doesn't matter, and the String object is just an uninteresting wrapper. And to push the comparison further: it is dubious, and hopefully impossible, to wrap a String object in another String object, just like it is dubious to have a promise for a promise.

 



 

 

<snip>

 

The main failure mode of standards bodies is to resolve a conflict by adding the union of the advocated features. Here, this works even worse than it usually does. The coherence of lifting depends on the absence of autolifting, and vice versa. We need to make a choice.

 

Indeed, the advantages I have mentioned vanished if we add unconditional lifting to autolifting + `then`, because its bare use forces the programmer to think what type of object he should provide (a promise or a non-promise) in some situations. If you allow me to do a somewhat shaky comparison, the usefulness of ASI is greatly reduced by the few situations where ASI does not work; therefore most style guides recommend to not use ASI at all.

 

 

—Claude

 

 

 

 

--
    Cheers,
    --MarkM



 


_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Andreas Rossberg-4
In reply to this post by Mark S. Miller-2
[+es-discuss]

On 10 May 2013 21:05, Andreas Rossberg <[hidden email]> wrote:

> On 10 May 2013 14:52, Mark S. Miller <[hidden email]> wrote:
>> An upper case type variable, e.g. T, is fully parametric. It may be a
>> promise or non-promise.
>> A lower case type variable, e.g. t, is constrained to be a non-promise. If
>> you wish to think in conventional type terms, consider Any the top type
>> immediately split into Promise and non-promise. Thus type parameter t is
>> implicitly constrained to be a subtype of non-promise.
>
> Mark, I'm afraid such a distinction makes absolutely no sense in a
> world of structural types. How would you specify the set of "types
> that aren't promises" in a way that is compatible with structural
> subtyping?
>
> /Andreas
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Tab Atkins Jr.
In reply to this post by Mark S. Miller-2
[Forgot to pick up es-discuss in this message. >_<]

On Fri, May 10, 2013 at 11:42 AM, Tab Atkins Jr. <[hidden email]> wrote:

> On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <[hidden email]> wrote:
>> Together with each of their explanations about what they meant. They are
>> both right. Lifting and auto-lifting do not mix. Q Promises give us
>> autolifting with no lifting. Monadic promises would give us lifting but no
>> autolifting. Having both in one system creates a mess which will lead
>> programmers into the particular pattern of bugs Jonas warns about in his
>> message.
>
> (For the benefit of those confused, "auto-lifting" is what has been
> termed elsewhere "conditional lifting". It's what Future.resolve()
> does: if passed a promise, it returns a new promise chained from it
> (as if you'd called p.then() with no arguments); if passed anything
> else, it returns a new promise that immediately accepts with that
> value.)
>
> I have no idea why you're trying to claim that "monadic promises"
> gives no auto-lifting.  Perhaps you've gotten yourself confused over
> the proposals, and have reverted to a particularly strict view of what
> "monadic promises" means?
>
> The proposal called "monadic promises" implies nothing more than that,
> if the value returned by a .then() callback is a promise, one level of
> unwrapping will occur.  If you purposely return a nested promise, we
> won't strip out *all* the levels, only the outermost.
>
> There's nothing about this that is incompatible with a conditional
> lift operator, a la Future.resolve().  Nor is there any reason to
> believe that the presence of Future.resolve() would lead to bugs.
> This confuses me - adding Future.resolve() makes nested futures no
> more likely (it can't create them), and *not* adding it makes some
> code patterns (where you want to accept either a value or a promise
> for the value, and work consistently in both situations) more
> difficult to code for.
>
>> * A Monadic promise system would have Q.fulfill, p.map, and p.flatMap.
>>
>> * A Q-like promise system would have Q, and p.then
>>
>> * The dominant non-Q-like proposal being debated in these threads has
>> Q.fulfill, Q, and p.then.
>>
>> This note explains why I believe the last is much worse than either of the
>> other two choices. As Jonas points out, generic code, to be useful in this
>> mixed system, has to be careful and reliable about how much it wraps or
>> unwraps the payloads it handles generically. Had programmers been armed with
>> .map and .flatMap, they could succeed reliably. Arming them only with .then
>> will lead to the astray. As an example, let's start with a variant of Sam's
>> async table abstraction. The parts in angle brackets or appearing as type
>> declarations only documents what we imagine the programmer may be thinking,
>> to be erased to get the real code they wrote. In this abstraction, only
>> promises for keys are provided. The get operation immediately returns a
>> promise for the value that will have been set. (An interesting variant is a
>> table that works even when the get arrives first. But we can ignore this
>> wrinkle here.)
>>
>>
>>     class AsyncTable<T,U> {
>>         constructor() {
>>             this.m = Map<T,U>(); // encapsulation doesn't matter for this
>> example
>>         }
>>         set(keyP :Promise<T>, val :U) {
>>             keyP.then(key => { this.m.set(key, val) });
>>         }
>>         get(keyP :Promise<T>) :Promise<U> {
>>             return keyP.then(key => this.m.get(key));
>>         }
>>     }
>>
>> When U is a non-promise, the code above works as intended. The .then in the
>> .get method was written to act as .map. When tested with U's that are not
>> promises, it works fine. But when U is actually Promise<V>, the .get method
>> above returns Promise<V> rather than Promise<U>. As far as the author is
>> concerned, the .then is functioning as a broken .map in this case, with the
>> signature
>>
>> p.then: Promise<T> -> (T -> U) -> Promise<V>  // WTF?
>
> Yes, if you're writing generic code that may return either a plain
> value or a promise, you have to wrap your return value in another
> promise.  The .then() magic (acting like either map() or flatMap(),
> depending on the return value's type) is more convenient for normal
> usage, but less convenient for people writing more generic code.
>
> The "unabashed monadic" version is more predictable for generic code,
> but less convenient for normal code (you have to be sure of whether
> your return value is a plain value or a promise, while .then() lets
> you ignore that for the most part).
>
> The Q version (auto-flattening all the time, no nested promises) is
> predictable both ways, but at the cost of erasing an entire class of
> use-cases where you want to be able to interact with a promise
> reliably without regard to what's inside of it.  (In other words, it
> becomes less predictable in regards to *time*, as what you thought
> would just be a retrieval delay might turn into a larger delay while
> the "inner" promise waits to be fulfilled.)
>
>> In a system with autolifting, you can't get full parametricity of promises
>> simply by adding an unconditional lift. You have to remove, or at least
>> strongly discourage, autolifting. Or you have to take pains to carefully
>> work around it.
>>
>> The main failure mode of standards bodies is to resolve a conflict by adding
>> the union of the advocated features. Here, this works even worse than it
>> usually does. The coherence of lifting depends on the absence of
>> autolifting, and vice versa. We need to make a choice.
>
> Nope, the correct way to write the code in the proposed version (with
> .then() and both types of lifting) is:
>
>      class AsyncTable<T,U> {
>          constructor() {
>              this.m = Map<T,U>();
>          }
>          set(keyP :Promise<T>, val :U) {
>              keyP.then(key => { this.m.set(key, val) });
>          }
>          get(keyP :Promise<T>) :Promise<U> {
>              return keyP.then(key => Q.fulfill(this.m.get(key)));
>          }
>      }
>
> Or, using Future terminology and erasing the types:
>
>      class AsyncTable {
>          constructor() {
>              this.m = Map();
>          }
>          set(keyP, val) {
>              keyP.then(key => { this.m.set(key, val) });
>          }
>          get(keyP) {
>              return keyP.then(key => Future.accept(this.m.get(key)));
>          }
>      }
>
> (Alternately, we could wrap the value in a promise in the set()
> function, and then just return it in the get() function.  It doesn't
> matter *where* the wrapping happens, just that it happens somewhere,
> if you want to be generic.)
>
> And we can do even better!  Let's make the two functions accept
> promises *or* plain values as keys:
>
>      class AsyncTable {
>          constructor() {
>              this.m = Map();
>          }
>          set(key, val) {
>              Future.resolve(key).then(key => { this.m.set(key, val) });
>          }
>          get(key) {
>              return Future.resolve(key).then(key =>
> Future.accept(this.m.get(key)));
>          }
>      }
>
> Easy and convenient!
>
> ~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Mark S. Miller-2
[dropping public-script-coord. Let's keep the discussion on es-discuss. Apologies again on starting this thread with an email to the wrong list.]


On Fri, May 10, 2013 at 1:00 PM, Tab Atkins Jr. <[hidden email]> wrote:
[Forgot to pick up es-discuss in this message. >_<]

On Fri, May 10, 2013 at 11:42 AM, Tab Atkins Jr. <[hidden email]> wrote:
> On Fri, May 10, 2013 at 5:52 AM, Mark S. Miller <[hidden email]> wrote:
>> Together with each of their explanations about what they meant. They are
>> both right. Lifting and auto-lifting do not mix. Q Promises give us
>> autolifting with no lifting. Monadic promises would give us lifting but no
>> autolifting. Having both in one system creates a mess which will lead
>> programmers into the particular pattern of bugs Jonas warns about in his
>> message.

[...]

> I have no idea why you're trying to claim that "monadic promises"
> gives no auto-lifting.  Perhaps you've gotten yourself confused over
> the proposals, and have reverted to a particularly strict view of what
> "monadic promises" means?
>
> The proposal called "monadic promises" implies nothing more than that,
> if the value returned by a .then() callback is a promise, one level of
> unwrapping will occur.  If you purposely return a nested promise, we
> won't strip out *all* the levels, only the outermost.

[...]

>
>> * A Monadic promise system would have Q.fulfill, p.map, and p.flatMap.
>>
>> * A Q-like promise system would have Q, and p.then
>>
>> * The dominant non-Q-like proposal being debated in these threads has
>> Q.fulfill, Q, and p.then.

[...] 

> The "unabashed monadic" version is more predictable for generic code,
> but less convenient for normal code (you have to be sure of whether
> your return value is a plain value or a promise, while .then() lets
> you ignore that for the most part).
>
> The Q version (auto-flattening all the time, no nested promises) is
> predictable both ways, but at the cost of erasing an entire class of
> use-cases where you want to be able to interact with a promise
> reliably without regard to what's inside of it.

Hi Tab, I did not intend to start a fight with you over the term "monadic promises". Since we seem to be otherwise in agreement on the three architectures to be compared as well as many of the implications of each, I'm happy to call the first bullet above "unabashed monadic promises", the second "Q-like promises" and the third "abashed monadic promises". Is this acceptable? If not, please suggest something you expect we can all find acceptable. AFAICT, this is the last terminology battle that still distracts us from semantics.

One request: If you do suggest alternate terminology, don't call #3 "monadic promises" while at the same time calling #1 "<adjective> monadic promises". That would make #1 seem like a subtype of #3. Either prefix both with an adjective or make them otherwise distinct.
 

--
    Cheers,
    --MarkM

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Tab Atkins Jr.
On Fri, May 10, 2013 at 2:04 PM, Mark S. Miller <[hidden email]> wrote:
> Hi Tab, I did not intend to start a fight with you over the term "monadic
> promises".

I didn't intend to start a fight either.  ^_^  I just wanted to make
sure no one was arguing against strawmen.

> Since we seem to be otherwise in agreement on the three
> architectures to be compared as well as many of the implications of each,
> I'm happy to call the first bullet above "unabashed monadic promises", the
> second "Q-like promises" and the third "abashed monadic promises". Is this
> acceptable? If not, please suggest something you expect we can all find
> acceptable. AFAICT, this is the last terminology battle that still distracts
> us from semantics.
>
> One request: If you do suggest alternate terminology, don't call #3 "monadic
> promises" while at the same time calling #1 "<adjective> monadic promises".
> That would make #1 seem like a subtype of #3. Either prefix both with an
> adjective or make them otherwise distinct.

I don't think we need to refer to "unabashed monadic promises" (your
#1) at all - nobody's seriously defending them, and they're far from
Promises/A+.  It's trivial to add map() and flatMap() to promises if
you want them, but .then() functions as a perfectly fine flatMap()
already if you maintain type discipline yourself.

The only serious proposals seem to be "Q-like promises", which
explicitly prevent nested promises by auto-flattening when necessary,
and "monadic promises", which allow nested promises if you explicitly
ask for it (but the flattening semantics ensure that it's hard to
accidentally fall into a nested case).

~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Mark S. Miller-2
Hi Tab, these are the three architectures I am trying to compare in order to make a point. Since you don't like the labels I've chosen for them, please suggest alternatives that should be clear and acceptable to all so that we can resume the conversation. Thanks.


On Fri, May 10, 2013 at 2:14 PM, Tab Atkins Jr. <[hidden email]> wrote:
On Fri, May 10, 2013 at 2:04 PM, Mark S. Miller <[hidden email]> wrote:
> Hi Tab, I did not intend to start a fight with you over the term "monadic
> promises".

I didn't intend to start a fight either.  ^_^  I just wanted to make
sure no one was arguing against strawmen.

> Since we seem to be otherwise in agreement on the three
> architectures to be compared as well as many of the implications of each,
> I'm happy to call the first bullet above "unabashed monadic promises", the
> second "Q-like promises" and the third "abashed monadic promises". Is this
> acceptable? If not, please suggest something you expect we can all find
> acceptable. AFAICT, this is the last terminology battle that still distracts
> us from semantics.
>
> One request: If you do suggest alternate terminology, don't call #3 "monadic
> promises" while at the same time calling #1 "<adjective> monadic promises".
> That would make #1 seem like a subtype of #3. Either prefix both with an
> adjective or make them otherwise distinct.

I don't think we need to refer to "unabashed monadic promises" (your
#1) at all - nobody's seriously defending them, and they're far from
Promises/A+.  It's trivial to add map() and flatMap() to promises if
you want them, but .then() functions as a perfectly fine flatMap()
already if you maintain type discipline yourself.

The only serious proposals seem to be "Q-like promises", which
explicitly prevent nested promises by auto-flattening when necessary,
and "monadic promises", which allow nested promises if you explicitly
ask for it (but the flattening semantics ensure that it's hard to
accidentally fall into a nested case).

~TJ



--
    Cheers,
    --MarkM

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Tab Atkins Jr.
On Fri, May 10, 2013 at 2:18 PM, Mark S. Miller <[hidden email]> wrote:
> Hi Tab, these are the three architectures I am trying to compare in order to
> make a point. Since you don't like the labels I've chosen for them, please
> suggest alternatives that should be clear and acceptable to all so that we
> can resume the conversation. Thanks.

Nobody is suggesting or wants a "pure" monadic option, so it's
unimportant what you call it.

~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Mark S. Miller-2
Fine. If there are no further objections, the three architectures are

Unabashed Monadic Promises
Q-Like Promises
Abashed Monadic Promises




On Fri, May 10, 2013 at 2:28 PM, Tab Atkins Jr. <[hidden email]> wrote:
On Fri, May 10, 2013 at 2:18 PM, Mark S. Miller <[hidden email]> wrote:
> Hi Tab, these are the three architectures I am trying to compare in order to
> make a point. Since you don't like the labels I've chosen for them, please
> suggest alternatives that should be clear and acceptable to all so that we
> can resume the conversation. Thanks.

Nobody is suggesting or wants a "pure" monadic option, so it's
unimportant what you call it.

~TJ



--
    Cheers,
    --MarkM

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: The Paradox of Partial Parametricity

Domenic Denicola-2
I found it [a fun exercise](https://gist.github.com/domenic/5632079) to show how little code it takes to build unabashed monadic promises on top of Q-like promises. (It's been a while since I exercised those brain-muscles, so any corrections appreciated.) The punch line is
 
```js
function unit(x) {
    return Q({ x });
}

function bind(m, f) {
    return m.then({ x } => f(x));
}
```

My interpretation of this exercise---apart from the fact that I miss doing mathematical proofs---is that, since it's so little code to implement unabashed monadic promises on top of Q-like promises, and Q-like promises have proven their worth in JavaScript whereas unabashed monadic promises have not, it makes much more sense to standardize on Q-like promises as the base, and leave unabashed monadic promises to user-space libraries.

(Abashed monadic promises are, of course, a failure mode of standardization---as Mark points out---and not really worth considering.)
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Tab Atkins Jr.
On Wed, May 22, 2013 at 6:04 PM, Domenic Denicola
<[hidden email]> wrote:

> I found it [a fun exercise](https://gist.github.com/domenic/5632079) to show how little code it takes to build unabashed monadic promises on top of Q-like promises. (It's been a while since I exercised those brain-muscles, so any corrections appreciated.) The punch line is
>
> ```js
> function unit(x) {
>     return Q({ x });
> }
>
> function bind(m, f) {
>     return m.then({ x } => f(x));
> }
> ```
>
> My interpretation of this exercise---apart from the fact that I miss doing mathematical proofs---is that, since it's so little code to implement unabashed monadic promises on top of Q-like promises, and Q-like promises have proven their worth in JavaScript whereas unabashed monadic promises have not, it makes much more sense to standardize on Q-like promises as the base, and leave unabashed monadic promises to user-space libraries.
>
> (Abashed monadic promises are, of course, a failure mode of standardization---as Mark points out---and not really worth considering.)

It's a weird wrapper object whose sole reason for existing is to
defeat the auto-unwrapping.  That's so ugly. ;_;

Try this proposal out instead:

Promises stack.  Nothing magical, they're just containers that can
contain anything, including more promises.

.then() waits until it can resolve to a plain value (delaying until
nested promises resolve) before calling its callbacks.  The callback
return values have the current spec magic, where you can return either
a plain value or a promise, and in the latter case the chained promise
unwraps it once and adopts its state.  You can return nested promises
here, but you'll never see them as long as you use .then().

.chain() has the same signature, but doesn't wait - it calls its
callbacks as soon as it resolves, with whatever value is inside of it,
whether it's a plain value or another promise.  Callback return values
have to be promises, or else it throws.  (This means that it's the
monadic operation, with no caveats.)

The remaining two methods, .done() and .catch(), match .then() for
convenience/conceptual integrity.

This proposal keeps the nice, conceptual simplicity of the promises
model, and gives you a choice of how to handle it, whether you want
the immediate underlying value or just the plain value after they all
resolve.  You can mix and match mid-stream if you'd like; you're not
locked into one or the other as soon as you start using one of them
(unlike the use-a-wrapper proposal).

This is, I think, a minimal surface-area, maximum ability proposal,
which addresses both the single-unwrapping use-cases and the
recursive-unwrapping convenience factor at the same time.  The methods
that people may be familiar with from their existing promise usage is
the "convenient" one with the nice magic, while "chain" matches the
nascent monadic efforts in JS and has a nice distinct name (which I
think communicates better about the behavior, too, where you have to
return a promise).

Thoughts?

~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: The Paradox of Partial Parametricity

Domenic Denicola-2
From: Tab Atkins Jr. [mailto:[hidden email]]
 
> Thoughts?

Sounds like a great user-space library!!
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

Kris Kowal-2

Tab’s proposal as I understand it is to standardize Q-like promises and add a "chain" method that is "then" but with behavior tailored for monadic composition.

This sounds like a good compromise.

The only downside I can contrive is that it gives users, particularly novices, a subtle choice. Would it be more clear that it is intended for monadic composition if the name were literally "bind"?

Kris Kowal

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: The Paradox of Partial Parametricity

Domenic Denicola-2
It also adds a fulfill method. Thus, it presents two interfaces to the user: fulfill + chain (aka unit + bind), and Q + then (aka resolve + then). This seems to squarely fall into the trap Mark described in his original post, viz.

> The main failure mode of standards bodies is to resolve a conflict by adding the union of the advocated features.

And indeed, I think the subsequent sentences ring just as true:

> Here, this works even worse than it usually does. The coherence of lifting depends on the absence of autolifting, and vice versa. We need to make a choice.

The point of my post was to demonstrate that fulfill/chain aka unit/bind could be built in user space *extremely simply*, thus allowing "the nascent monadic efforts in JS" to go off and do their own thing for a few years before asking to be baked into the platform. Promises, in the Q-plus-then-sense, have paid their dues. It's not very sporting for the monadic efforts to hijack the promise-standardization train, without first doing similar due diligence via real-world implementations and experience.

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: The Paradox of Partial Parametricity

David Sheets
On Thu, May 23, 2013 at 6:19 AM, Domenic Denicola
<[hidden email]> wrote:
> The point of my post was to demonstrate that fulfill/chain aka unit/bind could be built in user space *extremely simply*, thus allowing "the nascent monadic efforts in JS" to go off and do their own thing for a few years before asking to be baked into the platform.

JS is a compiler target. You are demanding that languages with
parametric polymorphism create extra garbage to map their semantics
onto your magic. You are demanding that authors who wish to create
parametric libraries jump through hoops and uglify their interfaces to
provide simple invariants.

> Promises, in the Q-plus-then-sense, have paid their dues.

Have they? Which languages have promises-in-the-Q-sense baked in?

> It's not very sporting for the monadic efforts to hijack the promise-standardization train, without first doing similar due diligence via real-world implementations and experience.

Haskell. OCaml. Any language with parametric polymorphism and static types.

Actual arguments are preferable over unsubstantiated assertions and
attempts at marginalization.
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
123