I grok that when implementing the details of a public function foo, especially if we need to add arguments like initializing an accumulator, we’re encouraged to call the private function do_foo. Fair enough. But sometimes when I’m down a few levels in the details, implementing the details of defp bar that was called from elsewhere, it seems a bit over-verbose to always make it do_bar.
One thing I tried on a few recent http://exercism.io exercises is to simply add an argument – not just to distinguish, which would be a horrid kluge, but to encapsulate the needed initialized accumulator, and this also provided a distinguishable “entry point” into the functionality.
For instance, the normal Enum.zip ends when either of the enums end, and I didn’t want that, so I made defp my_zip(first, second). This in turn called my_zip(first, second, ) (where of course the  is the initialization of an accumulator), rather than do_my_zip. All usage of my_zip other than that /2 “entry point” is /3, and there are no default args, so there’s no ambiguity. I had initially thought of calling it with an initialized accumulator, as I’ve often seen done, but that struck me as leaking an implementation detail. I then toyed with the idea of making the intially called thing actually /3 with a default initial accumulator, but I just wanted to try this approach, and it works fine.
So… your thoughts? What is generally preferred, in what situations, how strongly, and why? The alternatives I can identify offhand are:
Have the caller actually know what sort of initialized accumulator is needed. This way, my_zip(one, two) would not even exist. Instead, the caller would call my_zip(one, two, ). I’ve seen in most of the Elixir I’ve seen so far, but it seems “leaky”. Then again, it’s mostly been in exercises rather than “real” code, so maybe they weren’t really considering “proper” style.
Always prepend do_ for implementation details even of private functions. This way, my_zip(one, two) would call do_my_zip(one, two, ). This seems like making more names than necessary, and often longer.
Encapsulate the initial accumulator value as I did, by first calling a function taking only the args the caller would know about, and have that add the accumulator. This way, my_zip(one, two) would call my_zip(one, two, ). (The difference from the above being the lack of do_.)
Encapsulate the initial accumulator value with a default value. This way, my_zip(one, two) would actually be a call to defp my_zip(one, two, acc \\ ). This seems like it could run into trouble if there are other uses of the function with the lower arity, but for a private function in a smallish module it should be OK. It may also reduce the need for another function clause, but may also make it require a function head if there weren’t already any defaults.
I tend to go for #2, although #3 is also fine. The do_ thing is a bit clumsy, but I prefer it to having functions of the same name but different arities with one being public, and the other private. Although that’s maybe fine here, since they are doing the same thing.
I wouldn’t advise #1 (unless you specifically want a caller to pass the accumulator). Same thing for #4. Basically what you say - it’s leaky.
The do_ thing is pretty common. I have no data to back that claim, but I’ve seen it around, and use it myself. That doesn’t mean it’s a convention. My impression is that some people hate it, while others use it. I’m not a fan of it myself, but didn’t find a better alternative so far
As @sasajuric said, it’s pretty common – though like I said earlier, most of the Elixir I’ve seen has been academic/hobby exercises, not “real” (production) code, so maybe it isn’t as common as I thought, “in the wild”. I was also going to say something about the Elixir Style Guide I found at https://github.com/levionessa/elixir_style_guide but I think I misread when skimming that; what I was going to quote turns out not to be recommending implementing whatever's guts in do_whatever, unlesswhatever is public and do_whatever is private.
His my_zip needs a reverse as it is not directly returning the zipped list but pushing the values onto an accumulator so they are in the reverse order. Hence the reverse.
Which is better s a debatable, even the efficiency is not always clear. Pushing the values onto an accumulator means that the function tail-recursive which is good but you pay for it with an extra reverse. Directly returning the list is more straight-forward. Take your pick.
One time you do want to use the accumulator version is when you need to return multiple values in a tuple, then it can get messy if you return directly.