-
Notifications
You must be signed in to change notification settings - Fork 177
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
Bind quirk #329
Comments
I agree that this seems like a bug. Good find! |
On Sun, Apr 02, 2017 at 09:16:16AM -0700, Anton Bachin wrote:
In `Lwt.bind p f`, if `p` is already resolved and `f` raises, the `bind` raises. This is not consistent with the behavior of `Lwt.map f p` in the same circumstances: `map` pushes the exception into the new promise it evaluates to.
It is also not consistent with the behavior of either `bind` or `map` if `p` is not yet resolved. However, that inconsistency is a bit more defensible, because `bind` and `map` then can't run `f` before returning. But still, at least `map` has the same behavior whether or not `p` is resolved when `map` is called.
[...]
I think this is a bug. Thoughts?
This seems deliberate to me -- this is largely why Lwt.wrap exists -- and
something I keep in mind when I code. This semantics of Lwt.bind is
required to support tail calls properly -- otherwise, each bind on a resolved
promise (is that the correct terminology?) would push a stack frame for the
corresponding try ... with.
The function given to Lwt.map does not create new promises, so there's no
problem (and it is indeed safer) to wrap the exception in the Lwt monad.
…--
Mauricio Fernández
|
Yes, I realize that this makes the call to I've also been coding with awareness of how
It's not clear to me that this is an advantage of EDIT: Ok, that paragraph is not directly replying to the quote, more like replying to my opinion of the difference between the two functions, and why I think at least one of them is being too clever.
Yes :) |
Ah @mfp, I inserted a small edit into my comment above; I forgot that you might not see it if you reply only by email. Sorry, I won't edit like that again in the future. |
On Sun, Apr 02, 2017 at 10:38:29AM -0700, Anton Bachin wrote:
Yes, I realize that this makes the call to `f` a tail call, but it's not clear to me that that is very important, in particular that its importance in `bind` is so much greater than in `map`, that it's worth having different semantics between the two.
The difference is that a Lwt program is a really really long chain of Lwt.bind
calls evaluated by Lwt_main.run, and Lwt.map uses just an "immediate" function
that is expected to be well-behaved regarding stack usage :-)
Consider for instance `Lwt_list.iter_s f l` where f evaluates immediately and
l is a long-ish list.
> The function given to Lwt.map does not create new promises, so there's no problem (and it is indeed safer) to wrap the exception in the Lwt monad.
It's not clear to me that this is an *advantage* of `map` that makes it safer to not do a tail call. It seems more like a *disadvantage*: since `map` has to wrap the result of `f` in a promise, a tail call just happens not to be possible in `map` anyway.
My take is this: ideally, we'd want bind to capture "immediate" exceptions in
the right-hand function (and when I started using Lwt that was the semantics I
expected), but alas we can't have it in practice because we cannot rule out
the possibility of a long chain of binds being non-blocking.
For the sake of consistency, we could have `map` with the same semantics
as `bind` (Edit: with no gain regarding stack usage, because we have to wrap
the result anyway, as you said), but its current semantics is the one we'd want Lwt.bind to have if
it weren't impossible for practical reasons that don't hold for Lwt.map.
PS: my apologies if what I'm saying is obvious, I'm not too lucid today :)
--
Mauricio Fernández
|
No, those are valid points. It helps to have them clearly stated :) I also originally expected bind to capture "immediate" exceptions, when I was first learning Lwt. IMO, it is a sign of a serious design flaw that that we have to choose between:
The smallest reasonable change is probably to make I haven't fully thought this through, but it seems to me that a way to resolve this is not to guarantee that When you use
Basically, the current behavior of This is somehow notionally equivalent to running Essentially, tail recursion is not available in most of Lwt, including in Sorry if I wrote too much :) |
I guess this is an issue in open Lwt.Infix
let () =
let failed = Lwt.fail_with "foo" in
let pending, resolve = Lwt.wait () in
(* This catch leaks an exception from the handler, while the one for [p2]
pushes the exception into [p2]. *)
let p1 =
Lwt.catch
(fun () -> failed)
(fun exn -> raise Exit)
in
let p2 =
Lwt.catch
(fun () -> pending)
(fun exn -> raise Exit)
in
Lwt.wakeup_exn resolve (Failure "foo");
assert (Lwt.state p2 = Lwt.Fail Exit);
(* ocamlfind opt -linkpkg -package lwt catch.ml && ./a.out *) |
Another example of tail calls not happening in Lwt when they could be naively expected, this time due to wrapping function calls in |
This is probably something that Async got right (it doesn't "bind eagerly"). A related (and very nice) property is that it guarantees context switches can only happen in Maybe the "bind quirk" wrt. to map can be resolved (hah!) the other way... I think that both ensuring that OCaml-level exceptions are captured properly in the RHS of a bind (and the other functions you listed) and guaranteeing the context switch property by changing the wakeup semantics are valuable goals. Two conses come to mind:
Any others? (2) seems a priori trivial for practical uses cases, but (1) is more troubling. I used to think it was way too late for such a change, but after seeing how the deprecation scheme worked for 3.0, maybe, and if the change is deemed worthy, something similar could be done to e.g. introduce a new |
Yes, this is pretty much exactly what I want to do to all those functions.
For this, I thought to do some kind of study (exhaustive or probabilistic) of at least the code in OPAM. My hunch is that it will break very little actual code, though code in test cases or other contexts that don't depend on Also, I don't think we would be breaking the documented API, though breaking its undocumented behavior is still dangerous. Unfortunately, it's some kind of well-known fact in the community that
I would guess that the performance cost would be from extra allocations of deferred resolution queue nodes, and from caching issues due to resolving things in a different order than now. But, I thought about this in the last couple weeks, and we have at least one lever to mitigate this – we can make Lwt "finitely reentrant," where, let's say, up to 42 resolutions of a promise can be run without deferring. The extra cost would be maintaining this counter, which should be extremely cheap, and setting up an exception handler around each immediate call, which should also be cheap. After the 42nd resolution down into the current stack, the next resolution goes in the queue, i.e. it is treated like binding on something that isn't yet ready. In what I originally proposed above, 42 is instead 0 (no immediate resolutions allowed). The current semantics allow an unbounded number of immediate resolutions, which forces us to use tail recursion in I am pretty sure we can make this change using a combination of study, soft breakage (3.0.0 style), and performance tweaks, the latter of which might not even be necessary. The payoff would be much more predictable and teachable semantics. The above applies to But, since we already have these functions, we can just deprecate |
That's clever. It's nice to be able to make the presumed overhead arbitrarily small (modulo the costs you mentioned).
Also the fact that
Yes on second thought just promoting |
That too :) Hadn't seen that.
Actually, now I think it will still have to do that. If this is the top-level call to So it will be something like: run callbacks now, unless already running callbacks. I guess it will be that for |
To be more precise above, s/top-level call to |
On the matter of how to make such a change relatively gently, since we want to reuse the infix names and the PPX syntax (i.e. change their semantics), we can't really deprecate the existing names and ask everyone to patch their code repeatedly. I think, if the revdeps study shows that very few users will be affected, then we can go ahead with this. We will make some minor ( Simultaneously, we can make a branch of Lwt available with the new semantics already applied, and provide clear instructions on how to pin that branch, and a clear, justified explanation of what the change is and why we are doing it – a summary of this discussion. With the pin, a user can exercise their code base (run test suite, etc.) against the new semantics by running their normal build process. If exercising the code seems unproblematic (hopefully the typical outcome), then the user doesn't have to do anything except unpin Lwt. Otherwise, the user can constrain Lwt to |
What would be needed for the 3.1.0 milestone wrt. this? Making the branch with the new semantics available? Not sure how it ties to 3.1.0 effectively; it could be made available at any time (with enough advance) before (hypothetically) 4.0. Or to put it in another way, it doesn't seem to me 3.1.0 would block on this. What we can and probably should do already in 3.1.0 is try to encourage the transition from |
I agree that 3.1.0 doesn't block on this. To answer a hypothetical broader query, any issue can be dropped from a milestone if we don't have enough time, etc. – I just assigned this issue to 3.1.0 as a reminder to give the issue a hard look, in case we want to do 4.0.0 three months after 3.1.0, without an intervening minor release, and want to commit this change in 4.0.0. That might not be realistic anyway, because the revdeps study is going to take a while. But, to answer the actual specific concern that I believe you have: in 3.1.0 (or whatever minor release we have enough time to prepare this for), we should probably
So basically, it's because that minor release's announcements are maybe the best time to announce the change and the branch, and the I agree that we should probably discourage using |
I seemed to remember you'd rejected the idea of deprecating it before, but it was actually the renaming of |
Well, I think it's unnecessary as part of this specific issue, but I would like Basically, I want to reserve nice names like "resolve" for potential even larger semantic changes. |
Closing this for now as Lwt won't resolve it in the immediate future. See https://discuss.ocaml.org/t/1337/13 and #453 (comment). |
In
Lwt.bind p f
, ifp
is already resolved andf
raises, thebind
raises. This is not consistent with the behavior ofLwt.map f p
in the same circumstances:map
pushes the exception into the new promise it evaluates to.It is also not consistent with the behavior of either
bind
ormap
ifp
is not yet resolved. However, that inconsistency is a bit more defensible, becausebind
andmap
then can't runf
before returning. But still, at leastmap
has the same behavior whether or notp
is resolved whenmap
is called.In particular, as a consequence of the first paragraph, one cannot define
and get the same semantics as
Lwt.map
, becausemap'
will raise "eagerly" likeLwt.bind
does.I think this is a bug. Thoughts?
The text was updated successfully, but these errors were encountered: