reason
reason copied to clipboard
Syntax proposal: async/await
Except using lwt
instead of promises? orr we could not tie it to anything in particular and have it construct calls to async_map fn (val)
so you could do let async_map = Lwt.map
at the top of your file, or let async_map fn val = Js.Promise.then_ (fun v => Js.Promise.resolve (fn v)) val
.
Or maybe it should transform to bind
instead of map
?
Sounds good! Perhaps defining something like
module Async = Lwt
At the top of the file could be how you select the async "provider". I really like Lwt's syntactic ppx plugin, and we could provide first class syntax for it.
Hmmm actually I'm thinking that all I really need is ppx_let
https://github.com/janestreet/ppx_let with the proper setup.
Unfortunately it looks like reason doesn't support the 4.03 let%ext
syntax :(
does this not require a decision on what the standard for async should be ? I was under the impression that that was still not decided...
Actually, I've got a ppx together that isn't tied to any specific async implementation, as long as it adheres to a general signature.
@jaredly - have you thought more about this since July? I'm pretty interested in async/await syntax in reason -- the current async tools are a detractor for people coming from the JS side if you're used to having async/await. It would certainly make it an easier sell to re-write some of Expo's existing JS with Reason.
@brentvatne This has been at the top of our minds this week. There's a couple of approaches. Currently, I'm trying to weight the pros/cons between supporting syntactic extension for promise libraries like Lwt, vs. providing syntactic sugar for callbacks.
Syntactic sugar for callbacks would not full featured, and has some pitfalls - however, it is much lighter weight by default, and is simply sugar for supplying a final callback argument: "Lightest Weight Threads" has a nice ring to it: Any function that accepts a callback of some regular form would be compatible.
You could imagine the let!
keyword being used to indicate an async computation.
let asyncIncrement = (i, andThen) => andThen(i + 1);
let myFunc = (initialVal) => {
let! foo = asyncIncrement(10);
let z = nonAsync(20, foo);
lat q = asyncIncrement(z);
q;
};
let! finalValue = myFunc(200);
print_int(finalValue);
More examples, including how let!
/ and
could enable "parellel" tasks (concurrent, actually):
https://gist.github.com/jordwalke/1cd1f18ef060e2a5226f54f31417f6f2
We could instead build syntactic support for integrating with fully featured promise libraries such as Lwt
by using ppx_let
. I wonder if the two approaches are not incompatible. I wish there was a way for one to progressively enhance to the other.
I liked the Backcalls of LiveScript.
@jordwalke - the "Lightest Weight Threads" api looks elegant and easy to use. Coming from the JS side, my main concern is that when integrating Reason code with existing JS libs we'd want to be able to use this syntax with functions that return Promises, ideally without having to manually wrap them with another function for interop.
Suppose asyncIncrement
returns Js.Promise
, then let! foo = asyncIncrement(5)
would have a similar desugaring but rather than passing in a callback to asyncIncrement
it would pass it in to Js.Promise.then_
. Haven't thought about how to handle .catch
though.
@brentvatne Yeah, we'd want to create a way to interop with full featured promise libraries (including JS promises).
You could specify a promise provider perhaps:
let myFunction = () => {
open Promise;
let! thisIsTheResultOfJSPromise = asyncCall();
thisIsTheResultOfJSPromise + 10;
};
Or for short:
let myFunction = () => {
Promise.let thisIsTheResultOfJSPromise = asyncCall();
thisIsTheResultOfJSPromise + 10;
};
By default, you might consider that there's this hypothetical open Callback
by default which uses callbacks for the default behavior. (Or maybe the build system per project could decide the default).
Module.let
has the visual affordance of "Library Defined Let".
I really like the idea of Module.let
as it makes things very explicit and, when useful, makes it cleaner and more readable when multiple modules are to mixed - M1.let ...; M2.let ...; ...
.
A !
suffix on keywords is already used to signify that something is being overridden. It would be nice to make that use consistent, either by choosing something else as the let
suffix or changing open!
and friends to use a different syntax. That said, it may be nice to save a sugared let!
until the language can support it more cleanly with modular implicits or something similar.
For any syntax it would be nice to support the same for other keywords like switch
, try
, and if
. Lwt's ppx supports this and I think ppx_let does as well.
Lwt.switch promise {
| A => ...
| B => ...
| exception Not_found => ...
}
why not just use the same api as in js ?
let async f = () => {
let f = await somePromise();
someFunct(f);
}
This would require a standardized promise implementation.
I propose adding two syntax constructs which could be described as syntax sugar for ppx_let and therefore can used with any monadic structures (promises, option, result, ...).
-
let-do-bindings:
let N = do E;
-
do-expressions:
do E;
Syntax
Let-do-bindings
Reason:
let N = do E;
M
OCaml:
let%bind N = E in M
OCaml (after preprocessing with ppx_let):
Let_syntax.bind ~f:(fun N -> M) E
Let-do-bindings are used when you need to exract a value form a monad and define some new computations with it. For promises: "wait for a promise to be resolved, get its value and return a new promise based on it".
Do-expressions
Reason:
do E;
M
OCaml:
let%bind () = E in M
OCaml (after preprocessing with ppx_let):
Let_syntax.bind ~f:(fun () -> M) E
Do-expressions are used when you need to perform a series of side-effectful
computations for which result values are not defined (they are represented as
unit ()
and so you care only about the side-effects). For promises: "perform a
series of async actions on a filesystem: copy, move files".
Using syntax with concrete monads
ppx_let desugars let%bind
into Let_syntax.bind
calls. That means
Let_syntax
module should be in scope when using the proposed syntax
constructs.
I like how we can use local module open to bring the needed Let_syntax
into
scope:
module Promise = {
module Let_syntax = {
let bind = (~f, promise) =>
Js.Promise.then_(f, promise);
};
};
let getJSON = (url) => Promise.({
do AsyncLogService.debug("getting data...");
let resp = do fetch(url);
let data = do resp.json();
Yojson.Safe.parse(data);
});
Parallel bindings
let_ppx supports "parallel"-bindings:
let%bind x = getJSON("http://example.com/x.json")
and y = getJSON("http://example.com/y.json")
in ...
This maps on let-do-bindings:
let x = do getJSON("http://example.com/x.json")
and y = do getJSON("http://example.com/y.json");
...
The downside (?) is that do
keyword is required for each of the bindings in a
group. At the same time I don't think that allowing to omit it for and N
bindings is a good idea — it will bring less clarity.
Note that ppx_let implements parallel bindings in terms of Let_syntax.both
function. You can read more in its README.
Adding an implicit return value for the last do-expression
Currently in a chain of do-expression, the last value must be some monadic value. For example, with promises:
let copydir = (from, to) => Promise.({
do Fs.copy(from, to);
Js.Promise.resolve(());
})
We can add such return value implicitly in terms of the current Let_syntax
in
scope.
ppx_let doesn't have this feature so I'd describe it mapping from Reason to preprocessed OCaml code directly.
Note the refined definition of Promise.Let_syntax
which includes return
function to wrap any value into a promise.
Reason:
module Promise = {
module Let_syntax = {
let bind = ... as defined before ...
let return = (v) =>
Js.Promise.resolve(v);
};
};
let copydir = (from, to) => Promise.({
do Fs.copy(from, to);
})
OCaml:
let copydir from to = Promise.(
Let_syntax.bind
~f:(fun () -> Let_syntax.return ())
(Fs.copy fromb to)
)
Async-specific syntax vs. Monadic syntax
Simply, I believe async-specific keywords don't bring much value given that
do
-keyword already fits well with async computations (warning: I'm not native
speaker, I might be totally wrong about that).
The advantage is that we can use the proposed syntax with other monads than async computations:
- option - computations which may result in values which are absent
- result - computations which may result in errors
- ... other custom monads
The async computations use case already requires us to provide different "bindings" to the syntax. We want to define async computations at least with 3 different implementations:
-
Js.Promise
-
lwt
lib -
async
lib
ppx_let approach solves that elegantly with Let_syntax
-convention.
Good discussion. Here's some thoughts/questions:
- When would the value automatically be wrapped in
return
and when would it not? How would you opt out of that behavior? - The word
do
does imply side effect imho. It might not be the right word for things like async/option monads. - It's nice how
do
is one keyword whose position alternates its meaning betweenbind
and imperative bind.
When would the value automatically be wrapped in return and when would it not? How would you opt out of that behavior?
The only case, I can think of, which is useful to introduce an implicit return
is when do E;
is the last expression:
{
do E;
}
In that case instead of parsing it as:
let%bind () = E in ()
we parse it as:
let%bind () = E in [%return ()]
which is then transformed into:
Let_syntax.bind ~f(fun () -> Let_syntax.return ()) E
To opt-out — simply add a desired expression after the do E;
:
{
do E;
return(result);
}
Now the question is how to refer to the current return
, the obvious answer is via Let_syntax.return
which is in scope already (for ppx_let). That might be noisy so we potentially could extend the Let_syntax
convention to have a module Let_syntax.Open_in_body
which we will be opened in body of the monadic expression:
let N = do E;
M
will be transformed into:
Let_syntax.bind
~f(fun N -> let open Let_syntax.Open_in_body in M)
E
Similar thing for do
-expressions.
The complete example would look like:
module Promise = {
module Let_syntax = {
let bind = (~f, promise) =>
Js.Promise.then_(f, promise);
module Open_in_body = {
let return = (v) =>
Js.Promise.resolve(v)
}
};
};
let getJSON = (url) => Promise.({
let resp = do fetch(url);
let data = do resp.json();
return(Yojson.Safe.parse(data));
});
The downside is that return
will appear magically after the let-do
or do
syntax. It won't be possible to write:
let simple = (v) => Promise.({
return(v);
})
Maybe instead we should ask to put return
into Promise
directly?
(btw. In my prev comment I forgot to wrap the last expression of getJSON
into Promise
so that example fixes it)
The word do does imply side effect imho. It might not be the right word for things like async/option monads.
I think do
is generic enough:
-
option
—do
means "do try to unwrap" -
async
—do
means "do wait for resolution"
Now you can say that such "genericness" is not a good thing. But now I think we should've been arguing whether we need a generic monadic syntax or async-centered syntax.
Anyway the separate keyword do
(or another) on RHS looks to me like it's a better solution than a monadiclet
keyword (let!
or lat
or let%bind
) on LHS:
-
monadic
let
keyword on LHS maybe seen as it introduces bindings in some different way (related to scope), while having a monadic keyword on RHS hints that it is about binding to a value, not the nature of binding a name. -
I don't like punctuation in
let!
orlet%bind
.lat
is ok but it suites async use case only, I think.
I think it would be easy to explain do
for async computations for people with JS background: "it's like await
but called do
". Then later we can introduce the generic nature of do
for other monads (option
, result
, ...).
The syntax with local module open already looks very similar to async/await functions in JS:
let getJSON = (url) => Promise.({
let resp = do fetch(url);
let data = do resp.json();
return(Yojson.Safe.parse(data));
});
If we can somehow make local module open syntax "less noisy", then it gets even better:
let getJSON = (url) => Promise.{
let resp = do fetch(url);
let data = do resp.json();
return(Yojson.Safe.parse(data));
};
What about discussion in BuckleScript/bucklescript#1326? Citing proposed solution below:
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 *)
...
If I could throw my 2c in, as someone relatively new to the language, I dislike the use of[@bs]
. It is quite confusing. For someone who is simply trying to learn a new language and its paradigms, it greatly increases the cognitive load. It pulls in buckle script and ocaml into the discussion, and generally feels like a leaky abstraction. If the goal is to make reason an approachable language, especially to those coming from JS-land, I really think [@bs]
should be avoided, especially for commonly used constructs like promises.
I really like @andreypopp 's "do" proposal however.
I like @andreypopp 's "do" proposal too.
Getting local module open to be less noisy shouldn't be a problem.
It is not really clear to me what the scope would be for the different computations though: which opens are introduced by the sugar? Is it only about introducing the appropriate Let_syntax.bind
and leaving the scope unchanged?
Note: %bind let x = ...;
is already supported but the printing is not pretty. https://github.com/facebook/reason/pull/1703 tries to fix that.
For something like the do
proposal would something other than local open be possible? Some kind of annotation indicating what module should be used during the syntax transformation. This would help avoid unintentionally pulling values in. Something along the lines of (building on the example @andreypopp wrote):
module Promise = {
module Let_syntax = {
let bind = (~f, promise) =>
Js.Promise.then_(f, promise);
module Open_in_body = {
let return = (v) =>
Js.Promise.resolve(v)
}
};
};
let getJSON = (url) => {
/* Some syntax to say that we want to use Promise for any "do" syntax
without opening the module */
do with Promise;
/* Or some ppx-like [%do Promise]; */
let resp = do fetch(url);
let data = do resp.json();
return(Yojson.Safe.parse(data));
};
This would help avoid unintentionally pulling values in.
Maybe we can solve that by educating people not to have unnecessary values inside modules which provide Let_syntax
?
module Promise = {
module Do = {
module Let_syntax = {
let bind = (~f, promise) =>
Js.Promise.then_(f, promise);
}
};
};
let someOtherBinding = ...
};
let getJSON = (url) => Promise.Do({
let resp = do fetch(url);
let data = do resp.json();
return(Yojson.Safe.parse(data));
});
That means we won't need to introduce another syntax construct. Also it leaves power users with the ability to define some utilities which can be used along with do
-syntax w/o another open.
We could minimize the syntax impact by using something closer to existing syntax like let do = Promise;
One benefit of special syntax is that it makes it somewhat easier to find where the currently in-scope do
handling comes from. If it happens via open
, local or otherwise, then the source of Let_syntax
is harder to find.
@andreypopp @let-def What would you imagine we do for constructs like switch
?
If your proposal draws attention to the similarity between async/await, then I would imagine you'd have the following special cases for switch
/if
:
do M;
bind (M, ~f:(() => E))
let P = do M;
bind (M, ~f:(P => E))
switch (do M) {
| P1 => E1
};
bind (M, ~f:(fun P1 => E1));
if (do M) {E1} else {E2};
bind(M, ~f:(fun true => E1 | false => E2))
The thing I like about putting the keyword on the right hand side is that it's then very clear exactly which value is being "awaited on". The LHS approach does hint at what's actually going on with the heavy rewriting of let bindings - however people don't care about the details - they only care about their mental model of how async values work.
- I'm still not sure about the
do
keyword - it really does seem to imply effects. I am happy with a general purpose syntax transform for all monadic abstractions, but I wish the keyword conveyed that generality. Obscure symbols instead ofdo
could help keep it general. (I realize that these monadic binders really are imperative under the hood, but to the end user, they often allow a transformation into a more declarative style). - I don't know if implicit return should be used. I just don't have enough experience with Lwt to consider if it's a good idea. I wish others with more experience would weigh in.
- It's unfortunate to have to repeat the
do
keyword forand
bindings. - With the suggestions for
let x = do expr
andif(do expr)
I mentioned, people will expect that they will be able to "await" on any expression inline. Would you propose supportingdo expr
everywhere, and not merely special casingswitch/if
? That might be more involved. The left-hand-side syntax circumvents that problem.
Implicit return
seems like a bad idea. For example, the value may already be wrapped appropriately so adding return
would add an extra layer to the result.
For the record, here's the blog post I wrote on this subject recently: https://jaredforsyth.com/2017/12/30/building-async-await-in-reason/ (and accompanying ppx https://github.com/jaredly/reason_async) It touches on many of the topics discussed here.
(I'm also rather against a do
notation, I think it's far too difficult for newcomers to understand. I very much prefer verbosity in this case)
@jaredly What are your thoughts on async/await (in JS?)
I'm now enjoying Reason with let%ext
syntax. I'm thinking though if we can choose the more convenient char to type instead of %
— maybe let/bind
?
I like let/bind
(as an alternative to https://github.com/facebook/reason/pull/1735)
@let-def #1735 looks amazing! Even better than let/bind
. Would that syntax work if Reason decides to drop parens in if
, switch
and others?
@andreypopp I do not believe it will allow us to make parens optional for if
/switch
.
Is there any progress on this so far?
I also think that do notation and implicit returns might be a bad idea here.
@jaredly's blog post on the matter is awesome, but the illustrated implicit return requires introducing awaitWrap
and sacrifices the (rather helpful) explicitness of Promise.resolve
or an otherwise explicit keyword.
I can foresee the difficulty in explaining awaitWrap
to beginners who are not familiar with FP type signatures and knowledge of map and bind. It's much easier for their mental models if you just tell them "you're doing async stuff, so make sure to use this keyword to return the values".
Coroutines like Kotlin: Implements coroutines and let the rest builds around then, Kotlin coroutines infrastructure is amazing and lets you keep track of context.
I've never been a fan of the specialized syntax for promises in JS. Something like generators (coroutines) or do-notation are much more general and flexible. We'd do newcomers a disservice by assuming they can't learn/understand them.
@danny-andrews-snap have a look on my DSL proposal, this will make others additions like coroutines to don' t need a new syntax change to look like normal language syntax, https://github.com/facebook/reason/issues/1982
@danny-andrews-snap I know there can be elegance to treating all monads the same, but I think async/await and optional chaining are pretty compelling specializations (in javascript and rust, respectively). to my mind, it's not a matter of "this is too hard for newcomers to learn", and more "what will make things easiest to read & maintain for everyone". I'm not convinced that do-notation on its own accomplishes that.
My Continuation-passing style PPX is almost ready with suport for basic syntax tree constructions, async/await and any suspendable function can be implemented on top of it, no need to create new keywords or change the parser, its uses only one attribute at function declaration [@cps] and you can be monadic and await on a promise if you wish, but it is not necessary if you uses the continuation transparent style.
Would algebraic effects provide a primitive for this kind of thing? Or would it not translate to JavaScript? Or is that too far in the future like implicits?
What is wrong with how F# solves async/await for example?
https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/asynchronous-and-concurrent-programming/async
Like:
let fetchHtmlAsync = url =>
async {
let uri = Uri(url);
use webClient = new WebClient();
let! html = webClient.AsyncDownloadString(uri);
html
}
That is beautiful, everyone agrees?
take a look on, https://github.com/wiltonlazary/cps_ppx
@wiltonlazary I think if you documented it with extended README for simple API usage that would help
https://github.com/wiltonlazary/cps_ppx/blob/master/tests/src/js/IndexJs.re
I think that js only people dont know well the concept of coroutines that async computation can be built around
I think it's actually quite well known in the JS world, for example redux-saga
is like CSP with a single channel, and Node has a very popular library co
. Mapping to an implementation to a generator runtime (like the implementation of babel's one) would also work with not much effort. The question is Reason syntax.
ReasonML is going on direction of monadic computation for async, i dont think that folks here are very inclined to coroutines or the https://fsharpforfunandprofit.com/series/computation-expressions.html
@wiltonlazary informative link. The discussion is a bit disconnected but it does seem to be leaning toward generalized CPS. I personally admire F#. let!
however is explicitly monadic.
Being inspired by Haskell do
notation is also its shortcoming. It restricts you to a single Bind
per context. By comparison ppx_let
can intermingle bind
, map
, and both
.
both
is particularly interesting because it's for concurrent computations. Haskell uses apply
for this. both
in Haskell would be both = liftA2 (,)
or simply "apply each then tuple". Likewise Haskell provides sugar for apply
with a language extension called ApplicativeDo
.
I started by saying the discussion is leaning towards generalized CPS. Both let!
and ppx_let
expose a simultaneously principled yet limiting interface. The idea is to expose 1 normal function for each let
instead of 1 interface for all let
s in scope.
PS neat PPX. I think the syntax could be less verbose. A better forum for feedback would be reasonml.chat.
Computation expressions would be great as they are incredibly clean in F#. Also, try/catch in such implementation becomes much easier to reason about.
Try to not reinvent the wheel, my life is much easy now thanks to the fantastic implementation of coroutine concepts as a flexible framework by https://kotlinlang.org/docs/reference/coroutines.html
I have no clue if I am actually contributing, but I found this recent paper with the title Try/Catch and Async/Await are just a specialized form of Algebraic Effects! on Reddit and thought it might be relevant.
I have no clue if I am actually contributing, but I found this recent paper with the title Try/Catch and Async/Await are just a specialized form of Algebraic Effects! on Reddit and thought it might be relevant.
Yep, that's a pretty standard example of Algebraic Effects actually (as are faking mutable variables in an entirely immutable world).
OCaml has a HUGE side-developed system that includes one of the best Algebraic Effects systems I've seen short of having unbounded continuations and I hope to see it mainlined someday, but it seems slightly stalled (it would be SO useful though, especially combined with the multi-core changes).
But Reason would not be able to emulate Algebraic Effects at this time, however the OCaml work on it would no doubt mind another programmer working on it, thus meaning Reason would then just transparently support it as well. :-)
How will the recent merge of monadic and applicative let bindings in Ocaml affect this proposal? Can we just follow their lead?
The sanest solution would be introducing 'noawait' and turning each function which uses an async function into an async function automatically. Example:
// db.select() is known to return Promise
function foo(){ // compiler assigns return type Promise and adds await
return db.select("...") // if you don't want to wait for select to return use noawait db.select() (rare exceptional case)
}
rational: If you start turning your functions into async .. its easy to miss something (eg also when merging code). This all important cases would be caught by the compile.
The only problem would be if a function accepts any and any means Promises and values.. But that's an exceptional case. In such case you would need an explicit 'await', yes.
Given that let+
/let*
have been merged into OCaml 4.08 and backported through Dune to >=4.02, I think we can consider this issue resolved if and when Reason syntax also supports let+
/let*
etc. (I actually haven't tried it, maybe it already does).
As for BuckleScript, @bobzhang has an open issue for syntax sugar there https://github.com/BuckleScript/bucklescript/issues/1326
@yawaramin but would that mean having a real reasonml syntax for async/await that is truly isomorphic? ie. same syntax for both bucklescript and ocaml?
@benjamine I think we need to talk about them separately:
- ReasonML is (among other things) a new syntax for OCaml that provides one-to-one mappings for almost every OCaml syntax feature. As such, part of its mission is to support the
let+
syntax that is now officially in OCaml 4.08. This is because people will want to use ReasonML syntax to compile to native code (dune etc.). I consider the current issue (that we're on right now) to be about this. - BuckleScript is (effectively) a modified OCaml compiler that will soon be based on OCaml version 4.06, so it won't automatically get the
let+
syntax. What I suggested in that issue is to backport the syntax to BuckleScript the way dune does it for OCaml versions <4.08.
Once the above two issues are resolved, then yes, we would have the official OCaml let+
/let*
syntax supported universally.
@yawaramin oh, that makes sense, thanks for the detailed explanation 👍
Just tried an example from Let+ syntax backported to OCaml >= 4.02 post with OCaml 4.08.1 and Reason 3.5.0 @ 8ee1ff66, got not-so-user-friendly "Unknown error":
$ refmt --parse=ml
open Cmdliner
let ( let+ ) t f =
Term.(const f $ t)
let ( and+ ) a b =
Term.(const (fun x y -> x, y) $ a $ b)
let term =
let+ a = Arg.(value & flag & info ["a"] ~doc:"blah")
and+ b = Arg.(value & flag & info ["b"] ~doc:"blah")
and+ c = Arg.(value & flag & info ["c"] ~doc:"blah")
in
Printf.printf "a=%B b=%B c=%B\n" a b c
let cmd = (term, Term.info "foo" ~version:"v1.0.3" ~doc:"example")
let () = Term.(exit @@ eval cmd)
^D
Unknown error: please file an issue at github.com/facebook/reason
$ refmt --version
Reason 3.5.0 @ 8ee1ff66
$ ocaml --version
The OCaml toplevel, version 4.08.1
What's the latest status for let+ OCaml feature support? PR 2462 has promising title "OCaml 4.08 support" and is merged on Jul 4, Reason 3.5 @ 8ee1ff66 was released on Jul 15, so the PR should be included, but let+ syntax is not part of that PR?
OCaml syntax and reason syntax are separate so changes to OCaml syntax won’t be propagated automatically to Reason syntax.
I’d still want to have a similar feature in Reason as I find let-operators very useful in OCaml.
I understand that it won't automagically appear in Reason :) Just trying to understand the roadmap regarding this feature. Probably it's of low priority, as BuckleScript does not support that, and thus primary community of Reason won't receive any immediate benefit out of it. Makes sense.
Ppx_let mitigates this for native folks, just the less ppx you use, the better :)
This is something I honestly expected to arrive in Reason before OCaml, I remember discussions that made me think Reason async/await
was around the corner for a while now. But as an experience report, I can say that OCaml's flavor is working very well for me;
-
I like how lightweight the syntax is. For something this pervasive, I appreciate that it gets out of the way both when writing and reading code. When it's not clear, I have the type checker and Merlin.
-
I like the early convention established for both monads and applicatives, and how it doesn't use a keyword like
await
that makes any monad that's not promise-like look awkward. OCaml's implementation is not limited tolet*
andlet+
symbols by the way, but some convention goes a long way. -
I think it was the right choice to add it as an annotation on left hand side of let bindings.
await
in TypeScript in random places in right hand side or top level feels very awkward to me and limits the usefulness of this feature. But I know I'm in the minority. -
It maps cleanly to things like parallel bindings (
and*
), switch expressions, implicits etc.
Here's a contrived example:
let do_things = user_id => {
/* Using let in inner blocks won't block, but instead return a new promise.
This maps more cleanly to Promise.then in my opinion. I'm not sure if this
is possible with JS's `await`, plus you don't need to mark functions as
`async`. */
let some_operation = {
let* () = do_something(user_id);
let* () = do_some_other_thing(user_id);
return("done");
};
let* user = get_user(user_id);
/* There are run in parallel, like Promise.all: */
let* () =
if (user.active) {
update_last_access(user_id);
} else {
return();
}
and* friends =
switch (user.friends) {
| None | Some([]) => return([])
| Some(ids) =>
let* friends = get_users(ids);
let* () =
List.map(friends, ~f=user => user.id)
|> Lwt_list.iter_p(update_last_access);
return(friends);
};
let* result = some_operation;
return(result == "done");
};
OCaml syntax and reason syntax are separate so changes to OCaml syntax won’t be propagated automatically to Reason syntax.
What is the proposal that the team is favoring at the moment?
Putting OCaml's implementation aside, I would love to have something like https://github.com/jaredly/reason-macros as part of Reason. It can do anything ppx_let
or let%Anything
do, but it can also eliminate boilerplate like the examples in README do, and also do things like an Option.bind that compiles to a switch instead of creating a closure, which is nice to have for synchronous monads.
Some example macros for ReasonReact & BuckleScript:
[@macro.name "some.node"]
let%macro.let _ =
(pattern, value, continuation) =>
switch (eval__value) {
| None => React.null
| Some(eval__pattern) => eval__continuation
};
let%macro.let guard = (pattern, value, continuation) =>
switch (eval__value) {
| false => React.null
| true => eval__continuation
};
[@react.component]
let make = (~user) => {
let%guard () = user.city == "Amsterdam";
let avatar = {
let%some.node url = user.avatar;
<Avatar url />;
};
<div> avatar <span> {React.string(user.name)} </span> </div>;
};
let%macro nodeEnv = (value: string) =>
[%eval env("NODE_ENV")] == eval__value;
let endpoint =
if ([%nodeEnv "production"]) {
"https://www.example.com/graphql";
} else {
"http://0.0.0.0:8080/graphql";
};
I guess I'm late for this game. I made myself a PPX to resemble async
/await
in order to play with the language. But now that #2487 is merged, looks like my PPX will be more useful to plain OCaml/BuckleScript users than to ReasonML users.
Sorry for the spam, I'm sharing with the hope that it may be useful to someone.
With #2487 merged, this can probably be closed.
Before this issue gets closed, I think it would be good to have a reference example for anyone who lands on this issue from a Google search in the future. We should show a side-by-side example of async/await
in JS and the equivalent with Reason.
Perhaps it’s time for a blog post on the reason site as this issue definitely merits it and a link to that from here?