Very simply put (I am really sorry if this is a naive question): why are some functions of the Queue
module (standard library), namely copy
, iter
, and fold
implemented using a pattern
let f =
let rec f ...
in
fun q ...
?
To give a concrete example, the iter
function is implemented as follows:
let iter =
let rec iter f cell =
match cell with
| Nil -> ()
| Cons { content; next } ->
f content;
iter f next
in
fun f q -> iter f q.first
Is there any memory or allocation trick behind these choices of implementations? In the case of iter
what would be the difference with having
let iter f q =
let rec iter f cell =
match cell with
| Nil -> ()
| Cons { content; next } ->
f content;
iter f next
in
iter f q.first
The difference is that without optimisation, the second version allocates one closure for the inner iter
function everytime the outer iter
function is called.
With the native compiler the second version will be optimised into the first one anyway, but in bytecode you should be able to notice the extra allocation.
3 Likes
Ahh, interesting It makes perfect sense, thank you so much for your answer!
I wounder, why can’t we find this pattern among other mother in the Standard Library? Is it the fact that Queue
allocates more memory (given the representation of the data structure), hence we need to gain a little bit more somewhere else?
I suspect that it’s the only Stdlib module to have a recursive type wrapped in an outer record.
This means that one needs to write auxiliary recursive functions to traverse the inner structure, that do not need to be exported. In the other modules, I expect that the functions that are exported can be defined directly at toplevel, so no need for the inner function.
Have you seen other modules with the pattern let f x y = let rec f a b = ... in f x y
or similar ? If so, you can propose to switch to the better pattern with fun x y -> f x y
at the end, although I’m not sure it’s worth the trouble.