Typescript: Generalize `async`/`await` from `Promise<T>` to `Monad<T>`

Created on 16 May 2018  路  8Comments  路  Source: microsoft/TypeScript

Search Terms

  • async return
  • async return type

Suggestion

The async/await syntactic constructs are used to convert an asynchronous program into sequential looking code. Promise<T> is in fact a special case of what's called a Monad in the typed, functional programming community. And the async/await syntax is a special case of do notation in Haskell, or for comprehension in Scala.

It's a shame that the language restricts us to use this sugar only for Promises, when the more general Monad offers much more powerful abstractions.

I suggest generalizing Promise<T> to Monad<T> and the async/await keywords to monadic/extract (or better names):

interface Monad<T> {
  // same as the current `Promise<T>` interface
}

class Promise<T> implements Monad<T> {
  // implementation of `then`/`catch` as asynchronous computation
}

// other implementations are possible:
class Array<T> implements Monad<T> {
  // implementation of `then`/`catch` as non-deterministic computation
}

class State<T> implements Monad<T> {
  // implementation of `then`/`catch` as purely functional stateful computation
}

// Now we can use these like:
// non-deterministic greeter
monadic function greeter(str: string): Array<T> {
  const prefix = extract ['Hello', 'Hi'];
  const suffix = extract ['Welcome!', 'Nice to meet you'];
  return `${prefix} ${str}, ${suffix}`;
}

greeter('Me');
/* gives: [
 'Hello Me, Welcome!',
 'Hello Me, Nice to meet you!',
 'Hi Me, Welcome!',
 'Hi Me, Nice to meet you!',
] */

In fact, my suggestion is very non-intrusive and is only about renaming things and having one extra typing rule for monadic/extract (i.e requiring that the type is a Monad).

An optional extra typing rule is:

/* 
* pseudo typescript syntax :)
* It means that an optional type is monadic (implemented in the typing rules).
* AKA: `Maybe` in Haskell, or `Option[T]` in Scala
*/
type T? implements Monad<T> {
  // implementation of `then`/`catch` as possibly failing computation
}

The async/await keywords should stay to remain backwards compatible.

Use Cases

I originally wanted to implement a LazyPromise which would not automatically execute and would be executed only by a special method:

class LazyPromise<T> implements Promise<T> {
  // implement lazily
  run(): Promise<T> {
    // actually perform computation and return a regular promise.
  }
}

After that I wanted to make a ParallelPromise<T> which would analyze data dependencies and perform the computation in the most parallel way in the method run(..).

But I was disappointed to find out that the typing rules for async/await do not allow the return type to be anything other than the native Promise<T>, thus we lose the more refined type LazyPromise<T>/ParallelPromise<T>.

My suggestions allows for such implementations as well as a whole swath of other highly reusable abstractions, already existing in the functional programming community.

Examples

Implementing a Reader monad for read-only configuration/dependency injection

class Reader<C,T> implements Monad<T> {
  constructor(public run: (config: C) -> T) {}

  then(cb) {
    return new Reader(c => cb(this.run(c)));
  }
}

/**
  A `Reader` that returns the current configuration 
*/
function ask<C>(): Reader<C,C> {
  return new Reader(c => c);
}
/**
  retries @fn for a configurable no. of retries or returns @default
*/
monadic function retry(def: string, fn: () -> string): Reader<number, string> {
  const retries = extract ask(); // get configuration
  for(let i = 0; i < retries; i++){
    try{
      return fn(); // possibly throwing function
    } catch(e) {}
  }
  return def;
}

retry('Errored', fetchSomethingFromDB).run(5); // run with 5 retries

Checklist

My suggestion meets these guidelines:
[x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
[x] This wouldn't change the runtime behavior of existing JavaScript code
[x] This could be implemented without emitting different JS based on the types of the expressions
[x] This isn't a runtime feature (e.g. new expression-level syntax)

Out of Scope

Most helpful comment

I believe changing async/await to something else is out of scope, since that construct is already in the JavaScript syntax. Remember: _TypeScript is just JavaScript_

All 8 comments

I believe changing async/await to something else is out of scope, since that construct is already in the JavaScript syntax. Remember: _TypeScript is just JavaScript_

  1. I'm talking about introducing two new keywords in addition to async/await.
  2. ok, your'e saying that typescript will only implement ECMAscript features. That's fair. But at least generalize the typing rules for async to allow for Promise subclasses. such that:
    async function fn(..): LazyPromise<string> {...}
    is not an error.

async function subsumes any then-able (including subclasses of Promise) so you actually cannot have this in JS and TypeScript is just informing you of this.

class PromiseSubclass extends Promise{}
async function huh() { return new PromiseSubclass(r => setTimeout(r, 1000, "hi")); }

var p = huh();

p instanceof PromiseSubclass  // false
p.constructor === Promise     // true

Take a look at FantasyLand if you want proper monadic constructs. Add in a dose of sweet.js or a babel macro and you can have the syntactical sugar you are looking for, where you compile down to ordinary TS from your macro-enhanced code and type-check that.

@sean-vieira Are you saying that JS will automatically wrap my custom thenable with a native promise at the end? Is there a good reason for this?
Also I figure I can do what I want using generators. Not sure if this is fully supported in typescript though.

Because the various promises needed to interop back in the day and the ES Promise implementation is (basically) lifted whole-hog from the Promises/A+ specification. Since Symbol did not exist back then (and even now, it's a band-aid unless you take a name in the global Symbol.for namespace), the best thing that could be done was to define a protocol. The smallest protocol that fit the popular libraries was an object with a then method.

That said, it is very odd that .then respects Symbol.species, but async / await does not. @domenic, any memory of why this is the case?

> class PromiseSubclass extends Promise {}
> PromiseSubclass[Symbol.species] === PromiseSubclass
true
> new PromiseSubclass(() => {}) instanceof PromiseSubclass
true
> new PromiseSubclass(() => {}).then(x => x) instanceof PromiseSubclass
true
> async function huh() { await new PromiseSubclass(r => setTimeout(r, 1000, "hi")); }
> huh() instanceof PromiseSubclass
false

As noted earlier, adding new JS constructs is outside the scope of the TS project. I would recommend filing this under https://tc39.github.io/ecma262/ instead.

@dev-matan-tsuberi You may be interested in this. I would also prefer all compilers to just support a generic abstraction of the async/await pattern, that anyone could register a confirming interface for async/await pattern, that the compiler will know how to interpret, because their so many things that can benifit form being able to just use this pattern with the specialized class implementation of it. Especially with typing system,becomes even more powerfull.
https://docs.google.com/document/d/1QigfqvlM2vMNl-4szjuPj9tFXIQle2zsbrsnze9bAIU/edit?usp=sharing

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manekinekko picture manekinekko  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments

siddjain picture siddjain  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

seanzer picture seanzer  路  3Comments