Add "finally" function to Pervasives#1855
Add "finally" function to Pervasives#1855nojb merged 9 commits intoocaml:trunkfrom mseri:pervasive-finally
Conversation
stdlib/stdlib.mli
Outdated
|
|
||
| val try_finally : (unit -> 'a) -> (unit -> unit) -> 'a | ||
| (** [try_finally f g] calls [f ()] and returns its result. If it raises, | ||
| the same exception is raised; in {b: any} case, [g ()] is called after |
There was a problem hiding this comment.
What you write here does not hold. If g raises then it's not the same exception that is raised, it's the exception of g. (also no need for this colon in {b:).
There was a problem hiding this comment.
True. I'd rather reword it than hide the exception from g, what do you think?
There was a problem hiding this comment.
True. I'd rather reword it than hide the exception from g, what do you think?
Yes it's not a good idea, as you could hide e.g. an Out_of_memory or asynchronous exceptions like Sys.Break.
There was a problem hiding this comment.
Yes, that's exactly what I was thinking about
|
I would leave the uses in the compiler unchanged, I would rather avoid a bootstrap for such a small addition. |
|
I had understood that the fact that the signature of |
|
No, it should not be necessary to bootstrap if you do not use the added functionality in the compiler proper. |
| let result = (try work () with e -> cleanup (); raise e) in | ||
| cleanup (); | ||
| result | ||
|
|
There was a problem hiding this comment.
Personally, I find
match work () with
| result -> cleanup (); result
| exception e -> cleanup (); raise eeasier to read.
There was a problem hiding this comment.
I agree. I thought of keeping the changes minimal by just moving the old code from Misc, but I don't see why not update the style! I'll update the code, and remove the changes to the compiler as you suggested
|
I would like to argue for not leaving the copy in |
|
I have a backup of the branch, so it's easily done. I probably prefer to have just the implementation in |
|
Why you are at it, when you remove But I haven't seen the Real Bikeshedding start yet: are we sure that we don't want to label the argument(s)? |
|
@gasche I had written I did not name the arguments because I was not really happy with their names and because in |
Not only that... we might prefer a version where |
|
@xclerc I'd like that discussion though. Is it more ergonomic/used as |
|
I like the convention of naming the cleanup function so you could reorder, and |
|
Could we also discuss the opportunity of storing the backtrace raised by |
|
Why do you need to bootstrap if you change |
|
@mseri I guess might depend on your computations, but if you while Also, I cannot remember ever wanting the cleanup function (To be clear, it is not the terseness that I find appealing, but the |
|
To follow @yakobowski you could look at #374 for a discussion on which implementation for |
|
@yakobowski I completely agree on the use of If we settle on a form of |
|
I have read the discussion on #374. I am going to use that implementation as it is allowing for greater flexibility without too much additional burden, but I think I am still undecided in the balance between simplicity and flexibility. I think if we use this other version then making the three functions get different parameters gets too verbose. In that case I'd stick with |
|
Seems like this would help for #640 … 💯 |
It is not the case, |
|
Oh, yes, you are right! I'll update the PR |
|
I had to locally re-define |
stdlib/stdlib.mli
Outdated
| ?always:(unit -> unit) -> | ||
| ?exceptionally:(unit -> unit) -> | ||
| (unit -> 'a) -> 'a | ||
| (** [try_finally work ~always ~exceptionally] is designed to run code |
There was a problem hiding this comment.
The argument order should be the same in the signature and in the documentation.
stdlib/stdlib.mli
Outdated
| changes that would only make sense if the function completes | ||
| correctly).} | ||
| } | ||
| For example: |
There was a problem hiding this comment.
This example will get some people to think: I cannot understand it because I don't know the compiler codebase. In fact they probably could, but still it is not engaging. Could you use a realistic but simpler example, or at least using less unknown third-party functions?
There was a problem hiding this comment.
I agree, but I had not touched it on purpose. I will add a simpler example (if needed) after we settle on an implementation
stdlib/stdlib.mli
Outdated
| ~always:(fun () -> close_out oc) | ||
| ~exceptionally:(fun _exn -> remove_file objfile); | ||
| ]} | ||
| If [exceptionally] fail with an exception, it is propagated as |
There was a problem hiding this comment.
fails with an exception
(Maybe this could be formulated as: "Exceptions raised by exceptionnaly are not caught by this function, they will be propagated to the caller as usual."
There was a problem hiding this comment.
I like your rewording, I’ll use that
stdlib/stdlib.mli
Outdated
| If [always] or [exceptionally] use exceptions internally for | ||
| control-flow but do not raise, then [try_finally] is careful to | ||
| preserve any exception backtrace coming from [work] or [always] | ||
| for easier debugging. |
There was a problem hiding this comment.
[try_finally] is careful to preserve the backtrace of any exception it re-raises from [work],
even if `[always]` or `[exceptionally]` use exceptions internally. For more details,
see {!Printexc.raise_with_backtrace}.
There was a problem hiding this comment.
I'm not sure this implementation detail is worth spelling out in the documentation. That's what I expect from the OCaml system.
There was a problem hiding this comment.
I agree that I would expect it from a Stdlib implementation, but that‘s often not what happens: in fact, I went to check, and none of the implementations in the various standard libraries mentioned above does that (I am going to send a PR to all of them in the next few days though)
There was a problem hiding this comment.
raise_with_backtrace wasn't available until recently, so using it in stdlibs may require backward-compatibility hacks.
There was a problem hiding this comment.
True but I expect that the number of persons who would not use the function fearing that it might not be the case is border to nil. So I rather see it as documentation noise.
There was a problem hiding this comment.
There was a problem hiding this comment.
Note that saving and restoring backtraces has a small-but-nonempty performance cost (you have to copy the backtrace out and move it back). So I thought that being explicit about the behavior might be useful in documentation for cost-reasoning purposes. That said, I think that people that are very careful about performance in this way would go read the implementation anyway, and others would probably not notice the difference.
stdlib/stdlib.mli
Outdated
| {ul | ||
| {- [always], that must be run after {b any} execution | ||
| of the function (typically, freeing system resources), and} | ||
| {- [exceptionally], that should be run {b only} if [work] or [always] |
There was a problem hiding this comment.
I'm not sold on this signature. I don't see any advantage in it, the primitive is bundling too many feature in my opinion. Somehow I find:
try try_finally work ~finally:always with
| e -> exceptionally (); raise eto be perfectly fine and maybe even more clear; which is the semantics of your function if I understand correctly.
There was a problem hiding this comment.
I do agree with you. Keeping the simple version makes actually for clearer code imho, as you don’t need to carefully read the documentation to understand what is going on with exceptionally, and it also makes the implementation much simpler.
There was a problem hiding this comment.
The two versions are not equivalent when backtraces are enabled, if exceptionally terminates correctly but uses exceptions for internal control flow. On the other hand, the complexity reduction may still be worth it -- it's not like the rest of the stdlib is careful with backtraces anyway, and we could think of improving backtrace handling in the compiler/semantics instead of in the standard library.
|
I have updated the implementation by dropping the try try_finally work ~always with
| e -> exceptionally (); raise eor try try_finally work ~always with
| e ->
let bt = Printexc.get_raw_backtrace in
exceptionally ();
Printexc.raise_with_backtrace e btI have kept a link to Should I keep an example in the docstring, maybe something like the one in @xclerc comment? let first_line filename =
let chan = open_in filename in
try_finally
(fun () -> input_line chan)
(fun () -> close_in chan)It may be worth adding an example on the line of the second snippet above (without Concerning @xclerc comments about the signature, I am not convinced that going to the full generality of val try_finally : work:('a -> 'b) -> 'a -> always:('c -> unit) -> 'c -> 'bis worth. It reduces the number of closures needed to possibly none but complicates unnecessarily the function call. Regarding the other suggestion of passing the same input val try_finally : always:('a -> unit) -> ('a -> 'b) -> 'a -> 'bI think it would cover nicely a common use case, but I think the assumption on In this respect I find the current version val try_finally : always:(unit -> unit) -> (unit -> 'a) -> 'aexplicit enough and easy to understand, and I think that the fact that it usually requires to add two closures is a minor pain and does not affect the readability of the code. |
stdlib/stdlib.mli
Outdated
| caught, they will be propagated to the caller as usual. | ||
| In any other case [try_finally] will return the result of the | ||
| [work] funciton or re-raise its exception, preserving the | ||
| backtrace (For more details, see {!Printexc.raise_with_backtrace}). |
There was a problem hiding this comment.
I think this doc string is unnecessarily long-winded. Here would be my take on it:
try_finally ~always work invokes work () and then always () before work returns with its value or an exception. In the latter case the exception is re-raised after always (), preserving the backtrace (see {!Printexc.raise_with_backtrace}). If always () raises, this exception is not caught and may shadow one work () may have raised.
Or even shorter (I'm still convinced the discussion about the backtrace is pointless):
try_finally ~always work invokes work () and then always () before work returns with its value or an exception. In the latter case the exception is re-raised after always (). If always () raises, this exception is not caught and may shadow one work () may have raised.
There was a problem hiding this comment.
I think that if we use the term "re-raise" explicitly, then it is reasonable to assume that this implicitly suggests "with the same backtrace" -- and indeed I hope that we can move in the future to a world where reraise preserves the backtrace always.
|
Regarding the signature discussion I have no strong feelings about it. But here are a few cents. I initially did like the style of the ocaml unix book and in general when wrapping functions I'd go application style with However this is only nice when your function has a single argument. When it has multiple ones then you have to e.g. |
|
Thanks for the comments. I have updated the docstring with @dbuenzli suggestion and the |
|
Feel free to ignore this bike shedding comment: when the function is called |
|
Appveyor failed because it took too long to compile and run the tests: "Build execution time has reached the maximum allowed time for your plan (60 minutes)." |
|
FWIW now here's the one in guestfs: https://github.com/libguestfs/libguestfs/blob/2547df8a0de46bb1447396e07ee0989bc3f8f31e/common/mlstdutils/std_utils.mli#L328 Now what we really want is a return statement: https://github.com/libguestfs/libguestfs/blob/2547df8a0de46bb1447396e07ee0989bc3f8f31e/common/mlstdutils/std_utils.mli#L340 |
|
Is there any reason not to merge this? |
|
I realize now that @alainfrisch, who has been driving most of the standard-library issue, is not cc-ed in this PR, so maybe that's one reason why he hasn't given an opinion yet. (Or maybe people are in holidays in August or less active.) |
nojb
left a comment
There was a problem hiding this comment.
Looks good to go, except for a couple of small tweaks.
| external _get_raw_backtrace: | ||
| unit -> _raw_backtrace = "caml_get_exception_raw_backtrace" | ||
| external _raise_with_backtrace: exn -> _raw_backtrace -> 'a | ||
| = "%raise_with_backtrace" |
There was a problem hiding this comment.
This is nitpicking, but: I am guessing the _ is suppose to mean that these identifiers are "for internal use"; however this is already expressed by not exposing these values in the interface. Moreover, underscores in OCaml have another meaning (disabling warning 32), so I would rather remove them.
| external _get_raw_backtrace: | ||
| unit -> _raw_backtrace = "caml_get_exception_raw_backtrace" | ||
| external _raise_with_backtrace: exn -> _raw_backtrace -> 'a | ||
| = "%raise_with_backtrace" |
stdlib/stdlib.mli
Outdated
| If [finally ()] raises, this exception is not caught and may shadow | ||
| one [work ()] may have raised. | ||
|
|
||
| @since NEXT_RELEASE *) |
There was a problem hiding this comment.
Please write 4.08.0 instead of NEXT_RELEASE.
Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
`try_finally work ~always ~exceptionally` is designed to run code in `work` that may fail with an exception, and has two kind of cleanup routines: - `always`, that must be run after **any** execution of the function (typically, freeing system resources), and - `exceptionally`, that should be run **only** if `work` or `always` failed with an exception (typically, undoing user-visible state changes that would only make sense if the function completes correctly). I had to locally re-define `rab_backtrace`, `get_raw_backtrace`, `raise_with_backtrace` because I could not refer to `Printexc`. Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
As a result of the poll in #1855. The votes at the time of commiting are: - 18 for `protect ~finally` - 12 for `try_finally ~finally` - 1 for `finally ~cleanup` - 0 for `try_finally ~always` - 0 for `try_finally ~cleanup` Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
Signed-off-by: Marcello Seri <marcello.seri@gmail.com>
|
I would be inclined to merge once the CI passes. @gasche do you agree? (BTW, Alain is on holiday, so I don't expect him to chime in soon). |
|
@nojb: I don't have an opinion. Personally I find it too difficult to make decisions on standard library changes because the design space is very large and so subjective. When I don't see objective reasons to prefer one choice to another, I prefer to not make a choice at all. I'm glad that @alainfrisch (and you, apparently) volunteered to take on the burden of making those decisions, and I'll let you do just that. (I think that the Option/Result modules that went in today as part of this Standard Library Festival Week are just fine, and I'm glad that you merged them. Thanks!) |
|
Merged; unfortunately I didn't notice that the history was quite a mess; it would have been much better to squash before. |
|
Sorry, I was waiting for all the comments before squashing them |

This exposes
Pervasives.try_finallyfunction, already implemented in theMiscmodule and used in the compiler.This function is very often used to make sure that resources are clened up on failure, and practically reimplemented over and over in many modules and standard librarires.
The following are from different sources in the ocaml ecosystem:
From containers (CCFun)
From base (Base.Exn)
From batteries (BatPervasives)
From xapi-stdext-pervasives (Pervasiveext) (the same module is also in xen-oxenstored)
From Unix System Programming in Ocaml:
As you also see from the implementations, there is a tendency to already use
finallyto mention this function.In other languages this idea seems to vary: haskell has a
bracketfunction, python, javascript, C# and F# havefinallyas a keyword in the exception matching constructs, ruby usesensure.I think that even if it is not an optimal name, it would ease adoption. But I am happy to get any feedback on what could be the right name and implementation. See also the discussion in #640
An alternative implementation could be the one from @c-cube here: 6bfb795