tc39 / proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Other
375 stars 29 forks source link

Rename await.cancelToken and make it an operator #64

Closed bergus closed 7 years ago

bergus commented 7 years ago

Continued from elsewhere: @itaysabato wrote:

Just one last suggestion in regards to the actual issue at hand: what about something like the following?

await with cancelToken;

With this syntax the intention seems clearer - that henceforth the current function's execution is bound to the state of the given cancel token. Alternatively, you could use curly braces to define the scope of the binding to the token, but the extra nesting would be annoying - which leads me back to trying to extend async but with little more verbosity, e.g.:

async with cancelToken function foo(args..., cancelToken /* or whatever name you choose as long as its compatible with the async with statement */ ) {
    //...
}

@isiahmeadows commented:

I kind of like that idea, but I'm not sold on the keyword choice.

bergus commented 7 years ago

I previously had suggested a similar thing, and think we pursue that idea further. I absolutely love using the with keyword for this purpose, it fits naturally from a linguistic viewpoint. Next to

async function(args, token) {
    await with token;
    …

declaring an associated token for every await in the current scope, we could develop this further and allow

    …
    const res = await promise(args) with token;
}

to race the cancellation only with that particular awaited promise. (This usage might have grammar issues though with semicolon insertion if token is a grouped expression).

zenparsing commented 7 years ago

I find the await with syntax intriguing. I haven't been able to put my finger on why yet, but there's something that bothers me about using meta properties to create LHS references.

dead-claudia commented 7 years ago

Edit: @bergus I totally didn't catch you suggesting this also... :smile:


@zenparsing

(translated original email to Markdown, made a few edits)


In my opinion, it just seems like an awkward way to represent a declarative action within a declarative construct. Currently, the concept for using syntax to create LHS references is rather unprecedented, and there's no way to modify any readable internal value represented with syntax (like this). It is possible to set a read only binding prior to it being possibly read (like Function.prototype.call), but that is completely external.

Here's my idea: await <expr> with <cancel token> and a meta property await.cancelToken set to the possible cancel token in use, defaulting to a cancel token that never resolves.

So, instead of passing an argument to the caller and expecting them to handle it accordingly, have the language handle it like this. That's the gist of what I'm proposing.

Translating this into a usable Promise API equivalent isn't turning out elegantly from my own experimentation, though, so that's an issue. And IIRC await isn't a reserved keyword except in async functions, complicating things further.

dead-claudia commented 7 years ago

@bergus @zenparsing

Here's another idea, one that should work much more elegantly: set the cancel token declaratively in its own TDZ (this is the same as the function body's scope), outside of the function body. This will result in a more concise syntax, while preventing its modification later on, and completely avoiding any semblance of assignment inherent with await.cancelToken = <cancel token> and await with <cancel token>. Additionally, we can forego adding any meta property, since there's only one possible cancel token, one that you have to be able to reference.


First: modify async function syntax to accept these additional forms:

async function foo(arg, opts) with opts.cancelToken {
    // code...
}

const cancelToken = new CancelToken()

Promise.all(files.map(async file with cancelToken => {
    // code...
}))

const method = async function (arg) with this.cancelToken {

}

class Foo {
    get cancelToken() { return cancelToken }
    static get cancelToken() { return cancelToken }

    async foo() with this.cancelToken {
        // code...
    }

    static async bar() with this.cancelToken {
        // code...
    }
}

const foo = {
    cancelToken,
    async foo() with this.cancelToken {
    },
}

// etc.

The expression in with <expr> would be evaluated in a TDZ right after default parameters, and the result of that will be the cancel token used to evaluate it. If you need to evaluate something with a different cancel token within scope, it will be fairly straightforward:

// Previously
async function doSomething(mainToken, subtaskToken) {
    await.cancelToken = mainToken;
    await foo()
    await.cancelToken = subtaskToken;
    await bar()
    await.cancelToken = mainToken;
    return baz()
}

// With my proposal
async function doSomething(mainToken, subtaskToken) with mainToken {
    await foo()
    await (async () with subtaskToken => bar())()
    return baz()
}

It's slightly less pretty, but 1. it's simpler, and 2. it's not at all a common case, anyways.

Additionally, this will have no effect on purely Promise-based APIs. Here's the equivalent without async functions (mod timing):

function using(token, init) {
    return Promise.withCancelToken(token, (resolve, reject) => {
        Promise.resolve(init()).then(resolve, reject)
    })
}

function doSomething(mainToken, subtaskToken) {
    return using(mainToken, () => foo())
    .then(() => using(subtaskToken, () => bar()))
    .then(() => using(mainToken, () => baz()))
}

As for why I chose this way:

Here's why it doesn't conflict with existing grammar:

For async function declarations/expressions, anything other than whitespace or comments existing between the closing ) and opening { is currently a syntax error, even with line terminators.

async function foo(...args) /* anything here is a syntax error */ { /* ... */ }
async function (...args) /* anything here is a syntax error */ { /* ... */ }

Similarly, for async arrow functions, anything other than whitespace (mod line terminators) or comments between the arguments and => is currently a syntax error.

(...args) /* anything here is a syntax error */ => { /* ... */ }
arg /* anything here is a syntax error */ => { /* ... */ }

Additionally, if the headless arrow strawman were to progress, with is currently a keyword, so it would still be an error to use as an identifier. Worst case scenario, if with were to lose keyword status with headless arrows incorporated into the language, it could be converted into a contextual keyword mandating no line terminator between async, with, and the cancel token expression, so it could still be parsed. (This part also implies we're not tied to any particular keyword name like with.)