Add Computation Expressions (from f#) to javascript

Expand async/await in something like https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions where async is not a keyword but a value and the transformation is done through this value in the means of a builder pattern.

This would allow expanding async/await to other patterns like nullability (option monad),

Would this include the same ! suffixed keywords? let!, do! yield! or something else?

I wonder what the use cases of this are in f# projects? As JS already has dedicated syntax (which can be composed) for:

  • async tasks: async await
  • iteration: function* yield
  • nullability: ?. ??=

I'd leave the syntax up to the official body, I'm not sure how these are usually defined but I think the f# syntax would work so I wouldn't mind re-using them.

The advantage of CE's is that it's more abstract that async/await and thus more powerful and allowing userland to define a bunch of these, so for promises there is almost no extra benefit to this than pure async/await (await could be repaced with let! / we could use a similar keyword as await instead of let!) but more on that later.

For iteration there also might not be a benefit, I'd have to type it out more clear.

For null-ability there would be a benefit, imagine this piece of code:

function foobar() {
  const x = someFunction()
  if (x == null) {
    return null
  }
  myFunction(x)
}

This cannot be rewritten using ?. or ??=, however, you could build an option CE that could work such that the following snippet is equivalent to the above (notice the option function and const!

option function foobar() {
  const! x = someFunction()
  myFunction(x)
}

But nullability is not the only place where you could use this. E.g. Promises are actually doing 2 things: they return later (the async stuff) and they can be successfull / fail (the "result" stuff), if we were to have something like this we could build a CE that only does the latter so (expected) errors can be returned as a value instead of having to be thrown (similar to callback-style where errors were passed instead of thrown), while still allowing early return using CE so you don't end up with go-like code with a bunch of boilerplate to return an error if another function returned an error

const [value, error] = myFunction()
if (error) {
  return error
}

const [otherValue, otherError] = myOtherFunction()
if (otherError) {
  return otherError
}

Another benefit would be that you could use these CE's in "value position", so you could do something like

function myFunction () {
  const myPromise = async {
    await new Promise(resolve => setTimeout(resolve, 1000))
  }
}

which would allow to use await in a async function OR an async block, so you don't have to use an async immediately invoking function / extract an async function if you want to use the await syntax for something but can use an inline async block

Some related alternative proposals:

For the nullability code example there is optional chaining pipeline syntax ?> https://github.com/tc39/proposal-pipeline-operator/issues/159

function foobar() {
  const x = someFunction();
  if (x == null) {
    return null;
  }
  return myFunction(x);
}

would become

function foobar() {
  return someFunction() ?> myFunction;
}

Also of interest could be the async extension of the do-expressions proposal: https://github.com/tc39/proposal-do-expressions/issues/4

const file = do async { // or async do or something
    const response = await fetch('some-resource.txt') // the main purpose of a async do would be to allow await
    response.text()
}

For the nullability code example there is optional chaining pipeline syntax ?> Optional chaining support for the pipeline operator? · Issue #159 · tc39/proposal-pipeline-operator · GitHub

That would be nice, but basically you would need 3 different operators to be able to cover everything in this proposal and that is only for the nullability part.

Also of interest could be the async extension of the do-expressions proposal: Async variant · Issue #4 · tc39/proposal-do-expressions · GitHub

This would be awesome already, but this proposal is still more useful as it would allow it to be used for other proposals as well.

Some other stuff that can be moved behind this Computation Expressions that I didn't mention yet:

  • Lazy values (they come down to () => theValue but you could think about constructing more complicated lazy values without wanting to compute them already, this could be done using CE
  • async/await support for Non-greedy promises. Right now, Promises are greedy, the start running from the moment they are created. The user can still create something similar in userland but having good async/await support for these non-greedy promises is harder, especially if you want to use both type of promises mixed. The same goes for cancellable promises, ... Basically async/await is limited by the default promise and you can create non-default promises but async/await support for those is harder / doesn't exist. With a CE you could have a nonGreedyAsync function or cancellableAsync function
  • (React) hooks: right now they have to use a global to support this feature, if there were to be CE, they could build a custom one such that they don't need a global value anymore. There are plenty of other libraries that can use a custom CE as well probably and would get the same benefit as async/await brought but to many different problems

If you want to learn more, I'd try to find more about "monads" and which monads exist. CE are basically syntactic sugar for monads and applicatives but relatively easy to understand and avoiding the words "monad" and "applicative"

1 Like

Another related link that might be interesting to look at in the context of CEs is: https://github.com/pelotom/burrido

It tries to achieve FP monad-do style in JS using generators, though it only works if the code is pure.

Yes, I saw that one before but as you noted, it's full of caveats and will be very slow, especially for something like the list monad because generators are not cloneable / re-entreable.

Plus it's really hard to type generators for monad purposes. Both flowtype en typescript have a Generator<YieldedValue, ValueProvidedFromNext, ReturnValue> type, but for simulating monads, the ValueProvidedFromNext should be dependent on YieldedValue, e.g. if we map a Promise<number> into a Promise<string> and then in a Promise<boolean> then YieldedValue will be Promise<string | boolean> and ValueProvidedFromNext will be string | boolean but in reality we want it to be only string for the first yield and only boolean for the second. This is really hard to do with generators.

However with CE, this syntactic sugar just becomes function calls, just like await becomes a call to then on the promise you await, so typescript and the like can derive good types.

Basically, these CE would truly allow functional programming WITH types. Right now you have to choose between types or FP for some stuff but this kind of syntax extension would solve that problem forever, and would probably avoid adding a gazillion operators (?., ??, ?> to javascript, just because we're lacking computation expressions i.e. syntactic sugar for monads/applicatives)

To avoid 'breaking the web' I think async wouldn't be allowed as a custom CE builder otherwise it would change the behaviour of existing code.

// code that could exist on the web already:

let async = true;

async function sleep(ms) {
  await new Promise(resolve => setTimeout(resolve, ms));
}

if (async) {
  sleep(1000).then(() => console.log('async hello'));
} else {
  console.log('hello');
}

Also some additional syntax that signals when a CE is being used would likely be desired to avoid limiting future JS proposals too much. For example a proposal to add Map or Set literals

let s = Set {1, 2, 3};

would no longer be syntactic sugar that engines could optimise for as it would conflict with CEs.

Whereas if there was a dedicated indicator for when code is using a CE, this would restrict code from owning too much possible syntax, leaving room for JS to still evolve.

// example of more explicit CE usage
const file = using (cancellableAsync) {
  const response = await! fetch('some-resource.txt');
  return response.text();
}

To avoid 'breaking the web' I think async wouldn't be allowed as a custom CE builder otherwise it would change the behaviour of existing code.

Oh I didn't know that async could be used as a variable name, then indeed, we shouldn't allow overriding that :(

Also some additional syntax that signals when a CE is being used would likely be desired to avoid limiting future JS proposals too much. For example a proposal to add Map or Set literals

For this specific instance, Map or Set could actually be defined from a CE using the yield functionality (that's what the seq CE in f# does. You could get something very similar depending on actual concrete syntax (e.g. fsharp allows to drop the yield keyword in some circumstances)

But in general, I agree, I guess we could use it like cancellableAsync function () {} for the function syntax and following either do cancellableAsync or cancellableAsync do for block-syntax (similar to what Async variant · Issue #4 · tc39/proposal-do-expressions · GitHub is planning to do for async)?

Then we wouldn't "use" too much possible syntax.

A set could then be defined as

let s = do set { yield 1; yield 2; yield 3; };
// If yield ommission is supported
let s2 = do set { 1; 2; 3; };

If this was how we would define sets and maps, we have the advantage that userland could easily add other "literals" because it's just a matter of bringing in a certain value in scope and using that as a CE