Promises promises.. (making Stdlib.Lazy safe for programs with multiple domains)

So Orsetto is pretty old, and I’m currently messing around with a draft project that basically amounts to a complete redesign of its Cf library, adopts some programming patterns I’ve learned in recent years by reading the excellent work of others in the community, and targets OCaml 5 as the minimum version.

Orsetto uses Stdlib.Lazy in a lot of places, and that means it’s annoyingly fragile in programs with more than one Domain running simultaneously. So, I’ve been thinking about that, and I think what I want is a Promise data structure that wraps around Stdlib.Lazy with a concurrency attribute type that can be added as an optional argument in function types to help make safer programming interfaces.

The stupid little .mli file I wrote looks like this:

type attribute = private Pure | Fast | Safe of Mutex.t
type concurrency = [ `Fast | `Safe of Mutex.t ]

type +!'a t

val make: ?a:[< concurrency ] -> 'a Lazy.t -> 'a t
val get: 'a t -> 'a
val attribute: 'a t -> attribute

So I spent a couple minutes and drafted up a little toy that implements this interface in the mostly obvious way. I’m inclined to think my draft is a good candidate for nominating to include in the standard library, and I’m wondering what the community thinks about that idea.

It is not possible to achieve the optimal implementation by introducing wrappers around the current Lazy implementation. It adds unnecessary indirections and allocations. If the overhead is acceptable it might not even be useful to use Lazy (the difference between Lazy and what you can do by hand in OCaml are the runtime optimizations and the syntactic sugar, which you lose by wrapping).

Here is a possible solution for thread-safe lazys that keep the optimal efficiency and the lazy syntactic sugar in patterns: Discussing the design of Lazy under Multicore · Issue #750 · ocaml-multicore/ocaml-multicore · GitHub (perhaps more clearly than the middle of a discussion: other/ · master · gadmm / stdlib-experiment · GitLab). There are a couple more possible choices than you propose (e.g. how to synchronize, how to deal with exceptions).

Feel free to add to the discussion!

My goal isn’t to achieve the “optimal” implementation of thread-safe lazy. Instead, what I want is to facilitate easy introduction of locks for providing concurrency safe programming interfaces to existing logic that uses unsafe lazy under the interface, e.g. some persistent functional data structures that amortize costs using lazy evaluation.

On a similar note, consider the design pattern in the Logs.set_reporter_mutex function that @dbuenzli uses. Here, the idea is to make the costs of locking and concurrency safety paid on an opt-in basis. I want a similar thing for my libraries that use Lazy fairly extensively under the interface and I only want the costs of concurrency safety paid on an opt-in basis in the same way.

Accordingly, I can add ?a:[< Promise.concurrency ] arguments at top-level interfaces and store those values inside data structures where I would use 'a Promise.t instead of 'a Stdlib.Lazy.t. I can set the default in various places where those functions are used to Fast or Safe... as necessary.

I see. They are not exactly the same discussion. I suspect that basing your “locky” implementation on top of Stdlib.Lazy is not essential; but conversely, this exploration is interesting for the evolution of Lazy.