[ANN] Lwt.6.0.0~alpha (direct-style)

It is a great pleasure to announce the release of the first alpha release of Lwt 6. This major version bump brings two major changes to Lwt:

  • Using Lwt in direct-style! (Big thanks to @c-cube !!)
  • Using multiple Lwt schedulers running in separate domains!

Direct-style

This contribution from @c-cube is available in alpha00. It comes in the form of an lwt_direct package which provide an Lwt_direct module which provide two core functions:

val run : (unit -> 'a) -> 'a Lwt.t
val await : 'a Lwt.t -> 'a

and allows you to write code such as

run (fun () ->
  let continue = ref true in
  while !continue do
    match await @@ Lwt_io.read_line ic with
    | line -> await @@ Lwt_io.write_line oc line
    | exception End_of_file -> continue := false
  done)

There are a few more functions. All of which is documented in lwt_direct.mli.

Multi-scheduler

This addition is not available in alpha00 but should be added to alpha01 soon. It allows to call Lwt_main.run in different domains and benefit from actual parallelism. (Sneak peek in this pull request)

Installation

lwt.6.0.0~alpha00 and lwt_direct.6.0.0~alpha00 will soon be released on opam (PR on opam-repo. I’ll publish some more alphas as the work progresses, and announce the releases on this thread.

You can also pin the packages to the lwt-6 branch to get everything a little bit earlier:

opam pin lwt https://github.com/ocsigen/lwt.git#lwt-6
opam pin lwt_direct https://github.com/ocsigen/lwt.git#lwt-6

Feedback

Don’t hesitate to chime in on here with any feedback you may have. Ideas, comments, requests, suggestions, etc.

19 Likes

I’m guessing this should be while continue do, but it looks like an attractive option.

No, it’s !continue, to read the reference :slight_smile: . There are multiple ways to write this loop, but here it emphasizes the (new) possibility of writing direct style code that runs on Lwt’s scheduler and can wait on any Lwt.t promise.

2 Likes

As @c-cube mentions, it’s to read the reference. See OCaml library : Stdlib for official documentation on ! especially and OCaml - The core language for general info on imperative programming in OCaml.

2 Likes

D’oh! The perils of working in different languages all the time. I still can’t read it as anything other than ā€œwhile notā€ even now - the habit is too deeply ingrained. It’s switching between semicolon-terminated and not that mostly gets me for the first hour or two.

Thanks

I’m reminded of someone humorous explanations which may help with the correct interpretation.

How do you get a value out of a reference?
You bang(!) it.

Question: why in a separate Lwt_direct module? Why not in the main Lwt module itself? Is it because of the name run? Could we use Lwt.defer and Lwt.now instead?

In the current state of things, lwt_direct requires OCaml 5 while lwt doesn’t. I think otherwise it could make sense for Lwt_direct to live in the lwt library, just maybe not in the Lwt module itself — after all it’s not about promises as much as the moral equivalent of microtasks.

It’s an add-on to lwt offering a different way of thinking, while keeping full compatibility with the ecosystem. A separate module seems reasonable to me :slight_smile:

I’m not sure why you would pick names like defer and now. defer sounds like a resource handling function, and now like a clock function?

2 Likes

Oh OK, so the question is should Lwt 6 keep supporting OCaml 4?

defer is used in ZIO-Direct, which is an equivalent feature in Scala’s ZIO effect system: Introduction to ZIO Direct Style | ZIO

now is just a contrast to ā€˜defer’, as in ā€˜get the value later’ vs ā€˜get the value now’. ā€˜Defer’ terminology is also used by Jane Street Async–their promise type is 'a Deferred.t.

Maybe async and await would actually be better choices, but there is already Lwt.async so it might be confusing.

I think it’s a good question, since @raphael-proust has in the works a multicore PR (one Lwt event loop per domain). Even then I think a separate module would be good, inside the lwt library.

Thanks for explaining defer and now. I think await is non-negotiable, it’s almost universal at this point. run is a bit generic and I could imagine there being better names!

1 Like

Yeah, I can imagine there being confusion about Lwt_main.run vs Lwt_direct.run.

One more question: does try await promise with ex -> ... do the equivalent of Lwt.catch ie handle promise rejection?

May be worth mentioning, the Lwt module is available and very useful in the js-of-ocaml world where effects-compilation is even younger (even kinda experimental?).

2 Likes

Yes, Lwt_direct.await promise will raise the exception e if promise failed with e. It all works normally, try … with, match … with exception …, awaiting inside List.map, etc. Effects are awesome!

3 Likes

Yes, as mentioned by @c-cube the hope is to keep lwt compatible with ocaml4. It’ll likely require some more work on the multi-domain-multi-scheduler feature but I think it should be doable.

As for names, I think a separate module is nice, I can see Lwt_direct.run being renamed Lwt_direct.direct, or maybe the introduction of Lwt_direct.Syntax module which you are meant to open and which provides direct and await and yield so you write let open Lwt_direct.Syntax in direct (… bunch of code with awaits …). Suggestions definitelly welcome.

Oh kind of off-topic but is there a summary somewhere about how it’s done? Also, does it use generators at all?

Is the release of Lwt-6 a suitable opportunity to drop the Lwt_process module, as I don’t think it gives Lwt a good look?

That is because that module can no longer be safely used, as Lwt implicitly starts Thread.t threads when encountering blocking calls, and Lwt_unix.set_default_async_method is no longer able to change this in recent versions of Lwt. The problem is that Lwt_process calls up Unix.fork (via Lwt_unix.fork) and also Unix.exec*, none of which is thread safe.

About Unix.fork the OCaml documentation says ā€œ[Unix.fork] fails if the OCaml process is multi-core (any domain has been spawned). In addition, if any thread from the Thread module has been spawned, then the child process might be in a corrupted state.ā€

About the Unix.exec* functions, those can allocate memory with malloc and so are not async-signal-safe and so not thread/domain safe (Unix.create_process_env might not be multi-thread safe Ā· Issue #12395 Ā· ocaml/ocaml Ā· GitHub).

There are also some things in Lwt_unix which are unsafe and could be removed for similar reasons, such as Lwt_unix.fork and Lwt_unix.system.

To build an intuition, I would welcome an explanation what run and await do; also, it seems this can’t be implemented with OCaml 4 - but why is that?

I’ll check what uses of Lwt_process and Lwt_unix.fork there are in the wild opam-repository and consider that. Thanks for the suggestion!

The gist of it is:

run : (unit -> 'a) -> 'a Lwt.t creates a promise, sets up an effect handler to support await during the execution of its argument, executes its argument, resolves the promise with the result that is returned.

await : 'a Lwt.t -> 'a takes a promise as argument. Because of the way the Lwt promises work, that promise’s evaluation is either a resolved promise or a promise waiting for the scheduler to be awoken (when IO is ready or at the next tick). In the former case await simply returns the value. In the latter case await performs an effect. Upon receiving this effect, the handler installed by run attaches the continue callback to the waiting promise (via a simple Lwt.on_any). More specifically, the handler installs a callback to the promise, the callback puts the continue inside a queue, the queue is iterated upon when the scheduler starts a new tick.

There is a bit more to it, like exceptions. But that’s the gist of it.

This is not possible in OCaml 4 because you can’t punch through the scheduler, do some side-effects, and return to wherever you happened to be at.