How to break a for loop in a nice way

I want to do something like the following but in ocaml,

I someone can give an example code?

I’m not sure about how you define nice, but this is how I’d do it:

(* break *)
exception Break
let () =
  try
    for i = 0 to 9 do
      if i = 4 then
        raise Break
      else
        Printf.printf "%d\n%!" i
    done
  with
  | Break -> ()

(* continue *)
exception Continue
let () =
  for i = 0 to 9 do
    try
      if i == 4 then
        raise Continue;
      Printf.printf "%d\n%!" i
    with
    | Continue -> ()
  done
1 Like

Exit is an exception in stdlib defined exactly for this use case. It’s not raised by any library function so you can be sure you’re the one who caused it.

4 Likes

I am not sure from your recent questions whether it is ocaml’s facilities for dealing with imperative programming and mutable state which are perplexing you, or whether rather it is functional programming which is perplexing you.

If the former, then the examples you have been given are fine, although many might defer before calling it “nice”.

If the latter, the following is the more functional way to implement a break or continue equivalent in ocaml to the example C code to which you have referred, using iterative recursion. For your ‘break’ example in C:

let () =
  let rec loop i =
    if i <> 4 then begin
        Printf.printf "%d\n" i ;
        loop (i + 1) end in
  loop 0

For the ‘continue’ example in C:

let () =
  let rec loop i =
    if i = 4 then loop (i + 1)
    else if i < 10 then begin
        Printf.printf "%d\n" i ;
        loop (i + 1) end in
  loop 0

Is there anything here which would benefit from further explanation?

Edit For the ‘continue’ example this would be more efficient although less faithful to the layout of the C code:

let () =
  let rec loop i =
    if i < 10 then begin
        if i <> 4 then Printf.printf "%d\n" i ;
        loop (i + 1) end in
  loop 0
1 Like

@cvine Why is your code in the edit more efficient ? Although the tests “<10” and “==4” are done in different orders, it looks like they are done as often in both examples.

Yes, I misspoke. “More obvious” perhaps, in having a single point of recursion, rather than “more efficient” (save that it does omit the test against 4 when the count is 10).

1 Like

In this sort of situation, raise ought to be replaced by raise_notrace IMO. The distinction is that raise_notrace just affects control flow but does not involve the backtrace mechanism used to report unhandled exceptions. Unless the backtrace mechanism is disabled, raise_notrace is more efficient. It also does not clobber the existing state of the backtrace, and so (functions that call) raise_notrace can be used in exception handlers.

Also, while raise and raise_notrace are fast relative to the analogues in many other languages, pushing a trap handler (which has to be done every time a try expression is evaluated) is not entirely free. So do not be surprised if the let rec versions are slightly faster. On the other hand, returning early using an exception really shines when it jumps over the return from many pending non-tail function calls.

8 Likes

Can i consider “raise” as a “goto” where the “context” is dropped.

I’m curious, would using local exceptions here be any more efficient?

let f () =
  let exception E of int in
  try for i = 0 to 10 do
          if Int.(i = 5) then raise E i
       done;
       0
  with E i -> i

Whenever I have exceptions required for these kinds of early exits, I always use local exceptions, because I assume that it would provide additional information to the compiler - guaranteeing that the exceptions must have been generated from this particular invokation of the function.

I understand that vanilla OCaml doesn’t really do much optimisations, but would something like Flambda be able to make use of this?

4 Likes

Currently, no. In fact, local exceptions are only different from non-local exceptions during the scope resolution phase. It’s not an error to have a local exception escape its scope (either by raising it or returning is as a regular value, eventually embedded in a bigger value), and it is even possible to do a bit of introspection on out-of-scope exceptions to recover the constructor name for instance.

We do have some optimisations in flambda 2 when we can match a raise point with its handler statically, but it’s not specific to local exceptions. In your example we would be able to remove completely the exception mechanisms (except for a small detail at the definition point), but that would have been true even with a normal exception, wherever it had been defined.

I’d still advise for the use of local exceptions as good practice, but with the current compilers it is never faster than a regular exception and can be slightly slower in some cases (the definition itself is not completely free).

7 Likes