OCaml 5.3 decided to enable the effect syntax as follows:
match ... with
| effect e, k -> body
where e is the effect and k is the continuation. Personally I’m neutral on this because I understand the convenience of the syntax but also the desire to have developed effect system before designing good syntax. However, given that we made the decision, I wonder if we should also have specialized syntax for common cases where the continuation k is immediately resumed. The troublesome case is where the body directly raises an exception ex, which should be done by discontinue k ex instead to resume the continuation k.
The question is, what should be the concise syntax to enforce the use of discontinue for exceptions?
match ... with
| effect e, k ->
match body with
| v -> Effect.Deep.continue k v
| exception ex -> Effect.Deep.discontinue k ex
The main criticism is that this can create confusion because body (in the unexpanded code) can have a different type from other branches of match.
My Second Attempt (Radical)
I acknowledge the weakness of my First Attempt and was thinking about the following radical proposal:
continue ... with
| e -> body
The idea is that, we will have match...with for values, try...with for exceptions, and continue...with for basic effect handling. It does not cover the advanced usage where one wishes to save the continuation for later uses or to resume the computation with a different continuation, but it avoids the mistake of inappropriately raising an exception from an effect handler. I am posting my proposal here to see how the community feels about it.
In my opinion, the main point of effects is the ability to disrupt the control flow. With a syntax like continue ... with which only allows for immediate resumptions, you are throwing away the whole point of stack hopping. You could achieve the exact same behavior by using a plain closure stored in a thread-local variable. In fact, the plain closure is not only faster but actually more expressive because you can safely use it across the FFI frontier, contrarily to effects which will terminate your program with an exception. In other words, the syntax should not be changed to make it easier to use the wrong solution to a problem.
I don’t exactly have a horse in the syntactic sugar race, but I would like to say — although your suggestion is welcome @silene, I am a little confused by it:
I am not aware of any built-in facility for thread-local variables in OCaml. There is indeed a library (opam - thread-local-storage),. which is entirely undocumented…. Perhaps that is a solution.
Thread-local variables could address the use-case, but so could GOTO or callcc — or just monads (but perhaps you can see why not all solutions are equal). If you look at the literature and history, you will see that the purpose of effects & handlers has never been to make entirely new things possible that could not be expressed using other common languages features, but (1) to provide functional and structured abstractions for control, and (2) to allow for direct-style effectful programming without monad overhead, and (3) make the interpretation of effects modular. Each of these three motivations is mistreated, so far as I can tell, by the thread local variable idea.
Continuing on from my last point, if you have a look at the motivating literature on effects and handlers, you will see that indeed many if not most of the standard use-cases for effects and handlers appearing in the literature can be achieved with @favonia’s proposal.
It is certainly valid to be very happy that OCaml’s effects enable powerful and useful disruption of control like you say, and it is even valid to find simpler use-cases less compelling (like the classic “request something from the environment” / “resumable exception” use-case, which will probably dominate most non-library-code examples of effects in the wild). But unless I am misunderstanding something about the point you are making, it seems nonetheless unreasonable to make such assertions about these missing “the main point of effects”.
So although I again would like to reiterate that I don’t care too much about the syntactic sugar, I hope that we can have some openness in here about the different ways that people use (or intend to use) effects. Employing effects & handlers to make monadic code direct-style is absolutely a valid use-case, and I don’t think that it is reasonable to compare this with storing a thunk in a thread-local variable.
(Thanks @gadmm for the clarification, and my apologies for the mistake! I looked for a link to published documentation but I should have looked directly in the code. Will update my post accordingly.)
I disagree that the sole primary point of effects is to disrupt control flows.
Algebraic effects also enable modular, scoped, local reasoning about effects, which is the goal of this syntax. Thread-local or domain-local storage is essentially global storage. The strategy of saving closures in it (to avoid the need to find algebraic effect handlers on the stack) is similar to low-level POSIX signal handling, which is more efficient but makes it harder to reason about effectful code. I believe sacrificing local reasoning for speed is generally not a good trade-off.
We’ve built a compiler and proof assistant components that heavily use algebraic effects for various purposes. I personally find that algebraic effects often lead to more elegant and readable code than code with monads and/or any global storage (including thread-local and domain-local storage). More importantly, in most cases (though not all), an effect handler immediately resumes the given continuation (as covered by the proposed syntax). We can investigate how to implement specific algebraic effects more efficiently, perhaps by saving handlers in some global storage as suggested. However, I’d like to preserve the high-level interface.
Another point is that, in practice, the desire to disrupt control flows is largely satisfied by cooperative concurrent programming, and one can simply use one of the existing libraries (affect, domainslib, eio, fuseau, miou, moonpool, riot, …). Most users won’t need to write their own code that actively disrupts control flows. Yet, they might suffer from incorrectly raising exceptions from an algebraic effect handler, and the proposed syntax can significantly help them avoid these mistakes.
PS: As far as I can remember, the only exceptional cases not covered by this syntax but used in our code involve effect handlers that collect yielded values. The handlers build a sequence or a list by manipulating continuations. However, even in these cases, we implement the collector once in the base library, and no other code needs to worry about continuations.
You asked the community about the syntax you propose, and as a member of the community, I gave you my opinion. (That is, I strongly think that your use case is an anti-pattern for algebraic effects in an already effectful language, and thus I don’t think it should get a dedicated syntax.) There is no need for such a lengthy reply for every disagreeing opinion posted in this thread. Otherwise people might be discouraged from posting. Anyway, I do not design the OCaml language, so you do not need to convince me that my opinion is wrong.
I apologize if my previous response came across as dismissive. I appreciate your feedback and understand your concerns about the proposed syntax. Thank you for taking the time to share your perspective.