Skip to content

add the lwt_direct package, for direct-style control flow #1060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

c-cube
Copy link
Collaborator

@c-cube c-cube commented Jul 8, 2025

This is a WIP, it needs testing, but the fundamentals are here.

c-cube and others added 2 commits July 9, 2025 22:05
@c-cube c-cube force-pushed the simon/lwt-direct branch from 2b56bc6 to 8cab71e Compare July 10, 2025 13:51
@c-cube
Copy link
Collaborator Author

c-cube commented Jul 11, 2025

I added some tests!

@c-cube c-cube force-pushed the simon/lwt-direct branch 2 times, most recently from 0693b72 to 8586717 Compare July 11, 2025 03:04
@c-cube c-cube force-pushed the simon/lwt-direct branch from 8586717 to dc85027 Compare July 11, 2025 03:07
@c-cube
Copy link
Collaborator Author

c-cube commented Jul 11, 2025

5 |  (build_if (>= %{ocaml_version} "5.0"))
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: 'build_if' is only available since version 3.9 of the dune language.

😢 not sure how to make CI happy on OCaml 4 .xx at this rate…

edit: worked by doing the test with a manual rule

@c-cube c-cube marked this pull request as ready for review July 11, 2025 04:30
Copy link
Collaborator

@raphael-proust raphael-proust left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work.

I'll want to do a light editing pass with some renaming and such to make the code more homogeneous (e.g., in run "resolve" and "promise" should have consistent name). But I'll do that after we discuss the points that need discussing and we resolve some questions.

Comment on lines 15 to 25
{[
Lwt_direct.run (fun () ->
let continue = ref true in
while !continue do
match Lwt_io.read_line in_channel |> Lwt_direct.await with
| exception End_of_file -> continue := false
| line ->
let uppercase_line = String.uppercase_ascii line in
Lwt_io.write_line out_channel uppercase_line |> Lwt_direct.await
done)
]}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea from reading this example: make an openable module in direct and encourage bracketed use:

module Syntax : sig
  val run : (unit -> 'a) -> 'a Lwt.t
  val await : 'a Lwt.t -> 'a
end

and then

Lwt_direct.Syntax.(run @@ fun () ->

   wahatever code here the await is available

)

I don't think this is needed for alpha release. And also it shouldn't be documented here in an introductory explanation of the concepts of lwt-direct. But just puting this out there for the future.


Each task has its own storage, independent from other tasks or promises. *)
module Storage : sig
type 'a key = 'a Lwt.key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Do we want to export the type equality? It makes it possible to use Lwt functions to handle keys. Which at the very least is confusing but also would require to have tests in place.
  • Do we actually need to expose all of this considering it's all compatible with vanilla lwt storage? We could just say in the doc that all the Lwt.key-related functions are ok to use in run blocks.,
  • Should we actually advertise storage? it's deprecated so idk that we should encourage it much

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I think it's good to be able to use existing keys
  • it's deprecated but why? Local storage is important!! For logging and tracing at least. Eio has it too :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's deprecated because the way that the storage flows is non-obvious and not extensible.

Non-obvious:
Like even experienced Lwt users might have a hard time answering questions like:
In Lwt.on_success (fun v -> f v)) is f executed with the storage "of v" or the local ambient one?
What even is the storage "of v"? It's kinda ok to say if you have let v = <long series of bind>, but what if you have let v, r = Lwt.task ()? Does it get the storage from declaration site or does it gets it from resolution site?
What if you pass r around to several "threads" with different storages so that they race to resolve v?

It's the fundamental original issue of Lwt: there are no threads but the API sometimes say there is.

Non-extensible:
When you define something à la Lwt_list but for whatever data-structure you have in-house. Say Seqes or say lache. You'd want to be able to get storage and reset it so that the abstractions you build on top of your data-structure matches the behaviour of Lwt.
(E.g., in lache you would want to decide what happens if someone binds on a cached promise: do they get the storage from the "thread" which added the promise in the first place? do they get a clean-slate storage? you can't make that happen.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess actually you adding these storage function is because of the the lack of extensibility. So maybe the way out of it, the way to undeprecate is to

  • document clearly the built-in flow
  • provide the functions needed for extensibility
  • provide a few example and some documentation of how to properly do extensibility

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to write more about this, but in a way this storage makes more sense for Lwt_direct, where the scope should be clear (one call to run = one instance of storage). Even then there are questions about whether run inside run should inherit the storage or not? hmm

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I think the stoarge makes more sense for direct-style, because of the run wrapper gives it a more explicit scope. let's keep it for now and we'll see later

@raphael-proust
Copy link
Collaborator

merged the branch into lwt-6, which I want to use for the alpha releases.

i want to try and keep master clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants