I guess I don’t have much of an issue with it because I rarely use loops or if without else? It just never comes up as a problem for me. I’m usually constructing result types at the end of some conditional branch and it’s the end of that branch of the function body.
I could imagine this being a problem if you’re using OCaml’s crippled loop blocks. I don’t even try to use them and I don’t know why they put them into the language.
The loops are useful (and sometimes slightly faster) for imperative code. They’d be even more useful with early return but apparently not everyone wants that
They may be faster, but I really don’t see how they are any more useful than recursion, even for imperative code.
let my_array = [|3;5;8;13;21|] in
let rec loop i stop =
if (i = stop) || (Array.(i) > 10) then ()
else (Array.set my_array i (my_array.(i) * 3); loop (i+1) stop
in loop 0 (Array.length my_array)
I don’t find much less convenient than a for loop, and it has the benefit of being able to “return early”, so to speak. I always do it this way in my own code because I’m going to have to refactor to this in like 50% of cases eventually anyway.
Plus, if I don’t need an early return, I’m most likely going to be using a high order function anyway.
Array.iteri (fun i el -> Array.set my_array i (el * 3)) my_array
If other people like loop blocks and early returns, it’s fine. I just don’t use and feel the need for them, respectively.
Unless the code has been updated since you asked your question, there is no risk of out of bounds exception as the first if check if (i = stop) || ... then ... else ... where stop is always length my_array ensures that the value i is always a valid index.
For fun, we can also prove that the function is correct using the excellently made CFML2 library:
The program:
let update_array my_array =
let rec loop i stop =
if (i = stop) || (my_array.(i) > 10) then ()
else begin
my_array.(i) <- (my_array.(i) * 3);
loop (i+1) stop
end in
loop 0 (Array.length my_array)
The specification:
Fixpoint update_pure (ls: list int) :=
match ls with
| nil => nil
| h :: t =>
If h > 10
then h :: t
else h * 3 :: update_pure t
end.
Lemma update_array_spec (a: array int) (l: list int):
SPEC (update_array a)
PRE (a ~> Array l)
POSTUNIT (a ~> Array (update_pure l)).
Proof.
While loops with early exits can be achieved by putting the body of the loop in the condition part: for example, your code could be rewritten as
let my_array = [|3;5;8;13;21|] in
let i = ref 0 in
while
if !i = Array.length my_array || my_array.(!i) > 10 then false
else (my_array.(!i) <- my_array.(!i) * 3; incr i; true)
do () done
While generally speaking recursive functions are more flexible, While loops are useful when you need your code to be maximally performant. Roughly, the While loop above 1) does not allocate, 2) saves a function call on each iteration, 3) saves an indirection each time the free variable my_array is accessed.
This is relevant when writing high-performance code, and it is good to have the possibility of writing such code when you have the need. For example, at LexiFi, this style is often seen in the implementation of numerical primitives where we compete with C as implementation language.
Point 2 is not relevant for the discussion, since the generated assembly for the original code does not perform a function call either. Indeed, the compiler properly recognizes that idiom as a loop.
Point 1 is a bit unfortunate. Since there is no function call (cf above), a simple analysis would allow the compiler to notice that the allocated block is never used and thus can be optimized away (except for point 3).
Point 3 is the real issue here. There is indeed a costly indirection, though it can be avoided by using option -unbox-closures (assuming you are using the flambda compiler).
For the sake of completeness, note that if one were to manually unbox the closure (i.e., add a my_array argument to loop), then all the points become moot, even with the vanilla compiler: no allocation, no function call, no indirection.
I was about to ask if flambda didn’t fix some of this stuff, but I’m not much of a performance jock and wouldn’t be certain myself. I suppose I should find some resources about how to ensure my recursive loops are efficient, since it is my preferred idiom in OCaml.
I can’t… I have to invert the codition!!!
First of all, the first snippet is wrong because it assumes ; binds tighter than if-then-else (congrats @Gopiandcode you’ve proven an infinite loop to be bounded, no amount of verification can save you from wrong assumptions :P)
let rec loop i =
if i < Array.length arr && arr.(i) <= 10 then begin
arr.(i) <- arr.(i) * 3; loop (i+1)
end
also putting the while body in its condition to avoid installing exception traps is a cool trick, thanks for sharing
let i = ref 0 in
while !i < Array.length arr && arr.(!i) <= 10 && begin
arr.(!i) <- arr.(!i) * 3; incr i; true
end do () done
A naieve reading of the code by @ninjaaron reveals that it is not syntactically correct:
In particular [sic]:
else (Array.set my_array i (my_array.(i) * 3); loop (i+1) stop
is missing one closing parenthesis.
I interpreted this missing parenthesis as occurring after the loop, which is the only reading that makes sense, because otherwise the loop would indeed never terminate:
else (Array.set my_array i (my_array.(i) * 3); loop (i+1) stop)
I believe you’ve taken the other reading:
else (Array.set my_array i (my_array.(i) * 3)); loop (i+1) stop
which indeed leads to the infinite loop due to the weaker binding of ; than if then else.
I went looking for that hidden page again, and instead AI cited my remarks here as an unsourced memory of such a thing. So I looked a little harder, and I’m now sure that what I remember is an excerpt of Zwemer’s Arabia: The Cradle of Islam, chapter 9, “The Land of the Camel”, which remains a very funny thing to have found on OCaml’s website:
The Arabs highly value the camel, but do not admire its form and shape.
A horse complains in detail that the desert is a poor environment for it, and so a camel is created to answer each of his complaints, and–
The horse shuddered at the sight of what he wanted to become, and this is the reason every horse starts when meeting its caricature for the first time.
After these jibes, the camel’s design is very positively described. It’s a decent reply to the later joke that a camel is “a horse designed by committee”.