Idiomatic exceptions for exiting loops in OCaml -
in ocaml, imperative-style loops can exited raising exceptions.
while use of imperative loops not idiomatic per se in ocaml, i'd know idiomatic ways emulate imperative loops exits (taking account aspects such performance, if possible).
for instance, an old ocaml faq mentions exception exit
:
exit
: used jump out of loops or functions.
is still current? standard library mentions general-purpose exception:
the
exit
exception not raised library function. provided use in programs.
relatedly, this answer question mentions using precomputed let exit = exit
exception avoid allocations inside loop. still required?
also, 1 wants exit loop specific value, such raise (leave 42)
. there idiomatic exception or naming convention this? should use references in case (e.g. let res = ref -1 in ... <loop body> ... res := 42; raise exit
)?
finally, use of exit
in nested loops prevents cases 1 exit several loops, break <label>
in java. require defining exceptions different names, or @ least using integer indicate how many scopes should exited (e.g. leave 2
indicate 2 levels should exited). again, there approach/exception naming idiomatic here?
as posted in comments, idiomatic way exit in ocaml using continuations. @ point want return go to, create continuation, , pass code might return early. more general labels loops, since can exit has access continuation.
also, posted in comments, note usage of raise_notrace
exceptions trace never want runtime generate.
a "naive" first attempt:
module continuation : sig (* flaw approach: there no choice result type. *) type 'a cont = 'a -> unit (* with_early_exit f passes function "k" f. if f calls k, execution resumes if with_early_exit completed immediately. *) val with_early_exit : ('a cont -> 'a) -> 'a end = struct type 'a cont = 'a -> unit (* return implemented throwing exception. ref cell used store value continuation called - way avoid having generate exception type can store 'a each 'a module used with. integer supposed unique identifier distinguishing returns different nested contexts. *) type 'a context = 'a option ref * int64 exception unwind of int64 let make_cont ((cell, id) : 'a context) = fun result -> cell := result; raise_notrace (unwind id) let generate_id = let last_id = ref 0l in fun () -> last_id := int64.add !last_id 1l; !last_id let with_early_exit f = let id = generate_id () in let cell = ref none in let cont : 'a cont = make_cont (cell, id) in try f cont unwind when = id -> match !cell | result -> result (* should never happen... *) | none -> failwith "with_early_exit" end let _ = let nested_function k = k 15; in continuation.with_early_exit (nested_function 42) |> string_of_int |> print_endline
as can see, above implements exit hiding exception. continuation partially applied function knows unique id of context created, , has reference cell store result value while exception being thrown context. code above prints 15. can pass continuation k
deep want. can define function f
@ point passed with_early_exit
, giving effect similar having label on loop. use often.
the problem above result type of 'a cont
, arbitrarily set unit
. actually, function of type 'a cont
never returns, want behave raise
– usable type expected. however, doesn't work. if type ('a, 'b) cont = 'a -> 'b
, , pass down nested function, type checker infer type 'b
in 1 context, , force call continuations in contexts same type, i.e. won't able things like
(if ... 3 else k 15) ... (if ... "s" else k 16)
because first expression forces 'b
int
, second requires 'b
string
.
to solve this, need provide function analogous raise
return, i.e.
(if ... 3 else throw k 15) ... (if ... "s" else throw k 16)
this means stepping away pure continuations. have un-partially-apply make_cont
above (and renamed throw
), , pass naked context around instead:
module bettercontinuation : sig type 'a context val throw : 'a context -> 'a -> _ val with_early_exit : ('a context -> 'a) -> 'a end = struct type 'a context = 'a option ref * int64 exception unwind of int64 let throw ((cell, id) : 'a context) = fun result -> cell := result; raise_notrace (unwind id) let generate_id = (* same *) let with_early_exit f = let id = generate_id () in let cell = ref none in let context = (cell, id) in try f context unwind when = id -> match !cell | result -> result | none -> failwith "with_early_exit" end let _ = let nested_function k = ignore (bettercontinuation.throw k 15); in bettercontinuation.with_early_exit (nested_function 42) |> string_of_int |> print_endline
the expression throw k v
can used in contexts different types required.
i use approach pervasively in big applications work on. prefer regular exceptions. have more elaborate variant, with_early_exit
has signature this:
val with_early_exit : ('a context -> 'b) -> ('a -> 'b) -> 'b
where first function represents attempt something, , second represents handler errors of type 'a
may result. variants , polymorphic variants, gives more explicitly-typed take on exception handling. powerful polymorphic variants, set of error variands can inferred compiler.
the jane street approach same described here, , in fact had implementation generated exception types first-class modules. not sure anymore why chose 1 – there may subtle differences :)
Comments
Post a Comment