A Challenge Problem for Promise Designers

David Bruant bruant.d at gmail.com
Fri Apr 26 05:29:10 PDT 2013


Le 26/04/2013 13:24, Andreas Rossberg a écrit :
> On 26 April 2013 12:19, David Bruant <bruant.d at gmail.com> wrote:
>>> In particular, irregularity and exceptions become a pain
>>> when you start building abstractions, or plug together abstractions.
>>> In other words, regularity is a prerequisite for what some people
>>> (including me) like to call "compositionality".
>>>
>>> Flattening for futures/promises is irregular because it means that
>>> certain behaviour reliably exists in all cases _except_ when the input
>>> is itself a future/promise. This may not seem like a big deal if you
>>> use them directly. But now imagine somebody built a bigger generic
>>> abstraction that uses futures/promises only internally. You find that
>>> abstraction useful, and for whatever reason, need to funnel in a value
>>> that happens to be a future.
>> This last sentence and especially the ambiguous "for whatever reason" is the
>> heart of the debate I believe. The idea expressed by Kevin Smith of promises
>> as featureless values is that there should be no reason for you to funnel a
>> value that happens to be a promise unless it's expected to be a promise and
>> not a non-promise value.
>>
>> I have read somewhere (I can't remember where, hopefully MarkM will confirm
>> or say if I imagined it) that in E, if a variable contains a promise and
>> this promise is resolved, then the variable unwraps its value and
>> referencing to the variable suddenly means referencing to the value.
>> Promises are so deeply embedded in the language (syntax included) that this
>> unwrapping is seamless.
>> If I didn't imagine this, it'd be interesting if MarkM could expand on that
>> as it seems to imply that promises shouldn't be values but lower-level than
>> that.
>>
>> Even if he confirms, it probably won't be possible to have something that
>> embedded in JavaScript, but I think it's an interesting perspective.
> I'm not sure if your description of E is accurate -- I'd find that
> surprising. It _is_ a perfectly sensible design to have transparent
> futures that you can just use in place of the value they eventually
> get resolved to (let's call that value their 'content'). In fact, that
> is how futures/promises where originally proposed (back in the 70s)
> and implemented (e.g. in MultiLisp in 85, Oz in 95, and others later).
> However, there are really only two consistent points in the design
> space:
>
> 1. Either, futures are objects in their own right, with methods and
> everything. Then they should be fully compositional, and never be
> equated with their content, even after resolved (because that would
> change the meaning of a reference at locally unpredictable points in
> time).
>
> 2. Or, futures are entirely transparent, i.e. can be used everywhere
> as a placeholder for their content. In particular, that means that
> 'f.then' always denotes the 'then' method of the content, not of the
> future itself (even when the content value is not yet available).
>
> Unfortunately, option (2) requires that accessing a future that is not
> yet resolved has to (implicitly) block until it is (which is what
> happens in MultiLisp and friends).
I'm adventuring myself in places where I don't have experience, but I 
dont think blocking is what *has to* happen. Programming languages I 
know all have a local-by-default semantics, that is all values being 
played with are expected to be local by default (which sort of makes 
sense for in single-machine environments).
A programming language could take the opposite direction and consider 
all values as remote-by-default. If values are remote, with event-loop 
semantics, you never really block. Not more than is required to wait 
actual remote values (which is incompressible anyway). If you have local 
values abstracted as remote, the time you wait for the value is actually 
just shorter than when you're waiting for a value at the other side of 
the planet.
Promise pipelining [1] allows to interact with values (or give the 
impression to) although they're not here yet.

> That only makes sense with threads,
> so I don't see how it can be reconciled with JavaScript.
I agree, it's certainly way too late to retrofit "remote by default 
values" in JS. But the thought experiment is interesting. Maybe to be 
tested in a compile-to-JS language.

>> One thing I have been convinced by your message is that it seems necessary
>> to provide the lowest-level non-flattening primitives so that people build
>> abstractions can that consider promises as values and will want to know
>> about nested promise types. It should be possible, but building such
>> abstractions doesn't seem like the 80% use case.
>> Based on my experience, I remain pretty strong on the fact that flattening
>> is a sensible default and really has practical advantages to large-scale
>> development.
>>
>> So, what about the following:
>> 1) Providing lowest-level non-flattening primitives so that library authors
>> have fine-grained control and can build their own abstraction where they can
>> consider promises as values (what is missing in the current API?)
>> 2) The API to be used by the average devs (then/catch/done/every/any/etc.)
>> has flattening semantics as this is what people with the most experience
>> have been advocating for (people with a different experience are still free
>> to speak up).
>> This API would just happen to be one possible library that can be built on
>> top of 1), but one library which semantics has proven to be useful and
>> popular among devs.
>>
>> Would that satisfy everyone?
> Maybe, although I fear that the distinction isn't all that clear-cut,
> and library writers will still feel encouraged to build
> non-compositional abstractions on top of the high-level API.
True. I don't see that as an issue. Or more accurately, it doesn't make 
the non-compositional issue worse than it is today. However, providing 
both APIs, expert enough users will know what they should build on top 
of for the sake of good composability.
Non-expert users will keep making non-composable libraries; I think even 
only providing the low-level API couldn't save them from themselves :-)

> I have a strong feeling that the request for implicit flattening stems
> from the desire to somehow have your cake and eat it too regarding the
> choice between future semantics (1) and (2) above.
I don't know for others and about cakes too much, but I've had a ~8 
months experience working with promises on a Node.js application and 
I've experienced the benefits of not worrying about nested promises and 
really enjoyed it.
As a promise consumer (as opposed to a promise library author), I didn't 
miss promise<promise<T>> a single second. I wish the same experience for 
whoever is using web platform promises.
As many called out, where are the libraries with no-flattening 
semantics? Where is the experience with these libraries? What were the 
benefits and downsides of using these libraries?
I believe libraries converged to flattening semantics out of experience. 
I believe (though can't prove it formally) that non-flattening may have 
existed but proved inefficient when used at scale. More on that below.

> Even if you want
> flattening, isn't an explicit flattening method good enough?
I can't answer definitively by lack of experience with non-flattening 
semantics + explicit flattening method, but I'm afraid it will result in 
boilerplate:
Without flattening semantics, if a function changes of return value from 
Future<T> to Future<Future<T>>, then 2 things can happen:
1) The function does change of signature and all client of this function 
must call the flattening method
2) The function doesn't change it signature and calls the flattening 
method before returning

Some will forget to call it before return, we can imagine that in JS, 
some functions will return both Future<T> and Future<Future<T>> on 
different path (that's JavaScript, on the web, that'll happen, on 
purpose or by mistake) and suddenly, you end up calling the flattening 
function a lot (boilerplate). It wouldn't be surprising if blog posts or 
doc suggest to call it "just to be sure" at the beginning of every 
function taking a promise as argument (boilerplate as a good practice :-s).
In the end, instead of calling it all the time "just to be sure" or 
being afraid that your code break because of a change in nesting depth, 
you just wish that it was all taken care of by the promise 
infrastructure (what I called "hiding under the carpet" in an earlier 
post)...

I can't speak for them, but I believe what I've described (starting at a 
function changing of signature or a function being able to return 
different promise nesting level) is the reason popular promise libraries 
converged to a flattening semantics.

David

[1] http://erights.org/elib/distrib/pipeline.html


More information about the es-discuss mailing list