Skip to content

Add guidelines on judicious use of recursion #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Table of Contents:
* [Properly use logging levels](#properly-use-logging-levels)
* [Prefer the https protocol when specifying dependency locations](#prefer-the-https-protocol-over-others-when-specifying-dependency-urls)
* [Suggestions & Great Ideas](#suggestions--great-ideas)
* [Favor higher-order functions over manual use of recursion](#favor-higher-order-functions-over-manual-use-of-recursion)
* [CamelCase over Under_Score](#camelcase-over-under_score)
* [Prefer shorter (but still meaningful) variable names](#prefer-shorter-but-still-meaningful-variable-names)
* [Comment levels](#comment-levels)
Expand Down Expand Up @@ -538,6 +539,16 @@ handling.

Things that should be considered when writing code, but do not cause a PR rejection, or are too vague to consistently enforce.

***
##### Favor higher-order functions over manual use of recursion
> Occasionally recursion is the best way to implement a function, but often a fold or a list comprehension will yield safer, more comprehensible code.

*Examples*: [alternatives to recursion](src/recursion.erl)

*Reasoning*: Manually writing a recursive function is error-prone, and mistakes can be costly. In the wrong circumstances, a buggy recursive function can miss its base case, spiral out of control, and take down an entire node. This tends to counteract one of the main benefits of Erlang, where an error in a single process does not normally cause the entire node to crash.

Additionally, to an experienced Erlang developer, folds and list comprehensions are much easier to understand than complex recursive functions. Such contstructs behave predictably: they always perform an action for each element in a list. A recursive function may work similarly, but it often requires careful scrutiny to verify what path the control flow will actually take through the code in practice.

***
##### CamelCase over Under_Score
> Symbol naming: Use variables in CamelCase and atoms, function and module names with underscores.
Expand Down
38 changes: 38 additions & 0 deletions src/recursion.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-module(recursion).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went to go play with this and noticed that the functions aren't exported.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good call. I semi-deliberately left out the exports since I was mainly intending for this code to be a concise, illustrative example, rather than something anyone might ever actually want to run. I suppose it'd be easy enough to just add the one export line though and have a working module, so I'll go ahead and do that.

-export([recurse/1, fold/1, map/1, comprehension/1]).

%%
%% Example:
%% Different functions to capitalize a string
%%

%% BAD: makes unnecessary use of manual recursion
recurse(S) ->
lists:reverse(recurse(S, [])).

recurse([], Acc) ->

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is meant as an illustration of your point (if so 👍 😄 ), but this doesn't work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahaha I wish I could claim credit for intentionally writing a bug in this function, but admittedly I did not actually test the code since I was just trying to convey the gist of the idea, and didn't expect anyone to actually run it 😆 . I will fix it along with addressing your other comment above, and then do a rebase and push.

Acc;
recurse([H | T], Acc) ->
NewAcc = [string:to_upper(H) | Acc],
recurse(T, NewAcc).

%% GOOD: uses a fold instead to achieve the same result,
%% but this time more safely, and with fewer lines of code
fold(S) ->

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lists map lists:map(fun string:to_upper/1, S) would make the good example better (and almost the BEST ;-)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point! I had really wanted to demonstrate using a fold in my example, since almost any recursive list iteration function can be converted to a fold, so perhaps I will add a map example alongside fold.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Result = lists:foldl(fun fold_fun/2, [], S),
lists:reverse(Result).

fold_fun(C, Acc) ->
[string:to_upper(C) | Acc].

%% BETTER: uses a map instead of a fold to yield a simpler
%% implementation, since in this case a fold is overkill
map(S) ->
lists:map(fun string:to_upper/1, S).

%% BEST: in this case, a list comprehension yields the
%% simplest implementation (assuming we ignore the fact
%% that string:to_upper can also be used directly on strings)
comprehension(S) ->
[string:to_upper(C) || C <- S].