Async/await pattern matching

Related posts:

Topics asking for some kind of async/await appear from time to time, and it’s pretty clear there’s a segment of ReScripters very interested in such syntax features.

As far as I can understand, it isn’t done or actively considered because simple to use, simple to implement, an abuse-proof solution that gives a substantial advantage over Promise.then chains has not emerged.

To me, Promise.then is OK until subsequent thens require more and more computation results from the preceding clauses. This inevitably leads to highly-nested constructs which are hard to follow.

I’d like to share a thought that came to me spontaneously. If we use pattern matching to address so many problems in ReScript, could we also employ it for promises?

This is a legal ReScript code:

let main = () => {
  let foo = Js.Math.random()->Js.Math.unsafe_trunc

  let x = switch foo {
  | 3 => 43
  | 4 => 44
  | _ => 45
  | exception Js.Exn.Error(_) => 100
  | exception Not_found => 200
  }
  
  x
}

It compiles to:

JS Code
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';

var Js_exn = require("./stdlib/js_exn.js");
var Caml_js_exceptions = require("./stdlib/caml_js_exceptions.js");

function main(param) {
  var foo = Math.trunc(Math.random());
  var val;
  try {
    val = foo;
  }
  catch (raw_exn){
    var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
    if (exn.RE_EXN_ID === Js_exn.$$Error) {
      return 100;
    }
    if (exn.RE_EXN_ID === "Not_found") {
      return 200;
    }
    throw exn;
  }
  if (val !== 3) {
    if (val !== 4) {
      return 45;
    } else {
      return 44;
    }
  } else {
    return 43;
  }
}

exports.main = main;
/* No side effect */

What if we’d introduce an await keyword that might be used the same way as switch:

let main = () => {
  let foo = functionReturningIntPromise();

  let x = await foo {    // <- await instead of switch
  | 3 => 43
  | 4 => 44
  | _ => 45
  | exception Js.Exn.Error(_) => 100
  | exception Not_found => 200
  }
  
  x
}

that would compile to almost the same JS with two differences:

JS Code
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';

var Js_exn = require("./stdlib/js_exn.js");
var Caml_js_exceptions = require("./stdlib/caml_js_exceptions.js");

// DIFFERENCE
// ↓ async here
async function main(param) {
  var foo = functionReturningPromise;
  var val;
  try {
    val = await foo; // ← DIFFERENCE: await here
  }
  catch (raw_exn){
    var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
    if (exn.RE_EXN_ID === Js_exn.$$Error) {
      return 100;
    }
    if (exn.RE_EXN_ID === "Not_found") {
      return 200;
    }
    throw exn;
  }
  if (val !== 3) {
    if (val !== 4) {
      return 45;
    } else {
      return 44;
    }
  } else {
    return 43;
  }
}

exports.main = main;
/* No side effect */

What it gives? Access to the earlier outputs to avoid nesting:

let fn = () => {
  let foo = await readDataFromSource1() {
  | data => data["result"]
  }

  let bar = await readDataFromSource2(foo) {
  | data => data["val"]
  | exception Js.Exn.Error(_) => None
  }

  let qux = await readDataFromSource3(foo, bar) {
  | data => data
  }

  joinData(foo, bar, qux)
}

A mental pressure to check for error-path and use railway-oriented paradigm if one wants be closer to idiomatic FP:

let fn = () => {
  let foo = await readDataFromSource1() {
  | data => Ok(data["result"])
  | exception _ => Error(#Source1Failed)
  }

  let bar = foo->Result.flatMap(foo => await readDataFromSource2(foo) {
  | data => Ok(data["val"])
  | exception Js.Exn.Error(_) => Error(#Source2Failed)
  })

  let qux = (foo, bar)->ResultX.liftM2(((foo, bar)) => await readDataFromSource3(foo, bar) {
  | data => Ok(data)
  | exception _ => Error(#Source3Failed)
  })

  (foo, bar, qux)->ResultX.liftM3(/* ... */)
}

What do you think, folks? Just a topic to discuss.

12 Likes

Wow, your aproach looks really clean and easy to reason about. Would actually prefer something like this compared to let%bind. :grinning: