Rescript-compiler: implement monadic syntax sugar

Created on 7 Mar 2017  路  10Comments  路  Source: rescript-lang/rescript-compiler

see discussions in #1214

after some research, below is the syntax I think will be great and extensible in the future

let [@bs] v0  = promise0 in  (* default to Promise.bind *)
let [@bs] v1 = promise1 in 
let [@bs Option] v2 = optionl2 in  (* now default to Option.bind *)
let [@bs ] v3 = optional3 in 
let [@bs Promise] v4 = promise4 in (* back to promise *)
let [@bs error] v5 = error_handling in  (* here bind to Promise.catch *)
...  

We chose attributes instead of extension (like janestreet let%bind) because attributes allow payload for more customization later

todo:

research on semantics of let .. and

GOOD FOR PR PPX

Most helpful comment

There are some nice features in 4.08, so we may target 4.08.1 sooner than original timeline, we will see in next few months

All 10 comments

Bob, can we do something like what dune is doing and backport the new let* syntax? From that post:

The shim preprocessor converts bindings operators to OCaml identifiers of the form let__XXX and and__XXX. For instance, let+* is translated to let__plus_star. So you must make sure to not use such identifiers in your code.

I imagine this would be something like let$plus$star in BuckleScript output JS? We could have this shim in bsppx and get rid of it once BuckleScript lands 4.08+.

OCaml 4.08.0 is out. Would be really cool to have what @yawaramin suggested now!

There are some nice features in 4.08, so we may target 4.08.1 sooner than original timeline, we will see in next few months

@bobzhang, in order to be able to actually use let*/let+ for async binding, what are all the things that would have to happen? I was trying to dig into it a while back and this is what I understood from asking around:

  • We'd have to have a version of BSB that targets 4.08.1.
  • Refmt would have to parse let*/let+.
  • Bucklescript would have to recognize the language construct (or AST node?) and do the transformation of turning the code on the lines following the let into a callback.

Is that roughly correct? I apologize if my terminology is all wrong. I'm not very experienced in language development. But since you folks are busy making with other cool language features, I thought maybe I could help organize thoughts or efforts around this feature, and take some pressure off of you.

I think this bind sugar really matters to the growth of the community, especially those Javascript developers who are used to async/await and thinking of coming to Reason. I personally have found that I end up reaching for https://github.com/jaredly/let-anything in pretty much every new project that I start. It can drastically improve the readability of my code.

According to @anmonteiro this is what needs to happen before we can have a feature like this included in the language:

  1. refmt would need to upgrade to the 4.08 AST (it's currently on the 4.04 AST - I have a PR upgrading it to the 4.06 AST but it's more than a year old...)
  2. BuckleScript would need to upgrade to the 4.08 compiler if people using BuckleScript would like to take advantage of this syntax.

Is that in line with your thoughts @bobzhang?

On the native side, Dune has future_syntax (https://dune.readthedocs.io/en/stable/dune-files.html#future-syntax) to allow older compilers to use let+. That preprocessor could also be adapted. But it's probably "smarter" to switch to 4.08.

Has there been any progress on this front?

Is this now available as part of bs-platform?

Wanted to add my $0.02 on this, after the repackaging of ppx_let for rescript, working with bs-let for many months, and recently also using Reason's let operators through reason-repacked.

Right now, the following Reason code which uses a let extension:

let fn = () => {
  let%bind p = Js.Promise.resolve(42);
  let%bind x = Js.Promise.resolve(p * 2);

  Js.Promise.resolve(x + 42);
};

Is converted to:

let fn = () =>
  %bind(
    {
      let p = Js.Promise.resolve(42)
      %bind(
        {
          let x = Js.Promise.resolve(p * 2)

          Js.Promise.resolve(x + 42)
        }
      )
    }
  )

The original suggestion is to make it look like this:

let fn = () => {
  let [@bs] p = Js.Promise.resolve(42)
  let [@bs] x = Js.Promise.resolve(p * 2)

  Js.Promise.resolve(x + 42)
}

Or, if I use a module that provides an abstraction over promises for long running async operations:

let fn = () => {
  let [@bs AsyncOp] p = AsyncOp.execQuery(42)

  // then option 1
  let [@bs AsyncOp] x = AsyncOp.return(p * 2)
  AsyncOp.return(p * 2)

  // Or option 2
  let [@bs AsyncOp.map] x = p * 2
  (x + 42)
}

My thoughts:

  1. Correct me if I'm wrong, but no existing PPX that provides let binding (bs-let, ppx_let) will work out-of-the-box.
  2. I think there shouldn't be a default. It is obscure and uninformative, and Context is King. What's more, there are many async libraries out there, catering to different flavors, so the upside of having a default might end up being negligible.
  3. The error case is unclear.
  4. I like the idea of being able to support richer expressiveness through additional payloads.
  5. I think a let extension (a-la let%bind) is concise and meaningful with less typing than an attribute, esp. when the attribute requires _at least_ 4 characters (3 keystrokes) to begin with. With attributes, using a non-default module will require 4 keystrokes + the module name. When combined with modules like in ppx_let, you get a very robust system.
  6. Maybe a better, more DX-friendly approach, would be to start with a let extension, then when there is a need for a richer payload an attribute can be added. If there's a need for such a payload today, this approach could still provide a better DX. The problem with attributes is the typing overhead.
  7. As for the specific semantics, I think that the approach suggested by Jane Street in their ppx_let docs is very practical and has good DX. You define a module with the relevant operators, then you can open it for the let-operators to work in that context/space.

An example of the last point:

module AsyncOpSpace = {
  let bind = AsyncOp.bind
  let map = AsyncOp.map
  let return = AsyncOp.return
}

let fn = () => {
  open AsyncOpSpace // this provides context

  let.bind p = AsyncOp.execQuery(42)
  let.map x = p * 2

  (x + 42)
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

cknitt picture cknitt  路  5Comments

cknitt picture cknitt  路  3Comments

glennsl picture glennsl  路  3Comments

alexfedoseev picture alexfedoseev  路  5Comments

bobzhang picture bobzhang  路  4Comments