What I dislike about OCaml

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.

1 Like

In the context of git, it is partially solved by tools like GitHub - hhugo/merge-fmt: Git mergetool leveraging code formatters.

1 Like

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 :frowning:

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.

1 Like

Isn’t that what exceptions are for? :stuck_out_tongue_winking_eye:

1 Like

Doesn’t your code have a potentially out-of-bounds bug because you always recur with i+1?

i=0;
while (i<a.length && a[i]≤10) {
  a[i] *= 3;
  ++i;
}

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.

The full proof can be found at the following gist: Proof of correctness of update array ($2438833) · Snippets · Snippets · GitLab

2 Likes

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.

Cheers,
Nicolas

4 Likes

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.

2 Likes

Indeed! I forgot about this…

Cheers,
Nicolas

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. :thinking:

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

Aha, not quite!

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.

1 Like

Damn, I got QED’d :smile: (more than 20 chars)

1 Like

This is a good example of why I use beginend for sequencing blocks and not parentheses.

2 Likes

ocamlformat won’t let you use begin…end

Yes with the option exp-grouping=preserve OCamlFormat will respect your choice between parentheses and begin-end.

6 Likes

I’d rather use the defaults

2011 website: https://web.archive.org/web/20110926204429/http://caml.inria.fr/ , very attractive picture of a camel, and I think there’s a hidden excerpt from the Koran that praises the good design of the camel.

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”.

3 Likes