DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Coding
  3. Languages
  4. Erlang: functions (part 2)

Erlang: functions (part 2)

Giorgio Sironi user avatar by
Giorgio Sironi
·
Oct. 03, 12 · Interview
Like (0)
Save
Tweet
Share
4.69K Views

Join the DZone community and get the full member experience.

Join For Free
We know how to implement recursion, but it's often not enough to directly call a function from inside itself. Tail recursion is a functional programming idiom that lets functions recur on themselves for an unbound amount of times.

The stack

Let's take the definition of factorial from last week:
recursive_factorial(0) -> 1;
recursive_factorial(N) -> recursive_factorial(N - 1) * N.

We are recurring N times, where N is the number passed in the first call to recursive_factorial/1. Only when N is equal to 0 the first clause is matched and the recursion stops; afterwards, each call to recursive_factorial(0), recursive_factorial(1), ... returns and its result is multiplied for the argument N kept in memory until then.

You probably know that all function calls are piled up onto a stack, a data structure where the last inserted element is always the first to being pulled out. In the case of recursive_factorial/1, the stack looks like this:

factorial(0) -> % now I'll return 1
factorial(1) -> local: N = 1
factorial(2) -> local: N = 2
factorial(3) -> local: N = 3

when we are calculating recursive_factorial(3) and we are in recursive_factorial(0), of course.

The problem with this kind of recursion is that you may run out of stack space, *especially* if the function will continuously run and call itself, like a main loop. In C++, you could write:

while (1) {
    // wait for input, do something
}

but in Erlang doing the same with functions:

main() ->
    // wait for input, do something
    main().

would not be possible without tail recursion. The stack trace would grow every iteration, until we would run out of memory.

From direct recursion to tail recursion

The definition of a tail call in Erlang is:

The last statement of the execution of a function, whose return value becomes the return value of the original function.

For example, in:

a() ->
    b(),
    c().

c() is a tail call, while b() is not. If we wrote:

a(A) ->
    b(),
    A * c().

c() wouldn't be a tail call, because its return value would have to be processed to become a(A)'s return value.

When a tail call points to the function itself, it is an instance of tail recursion. Let's rewrite recursive_factorial:

tail_factorial(N) -> tail_factorial(1, N).

tail_factorial(N, 0) -> N;
tail_factorial(Result, N) -> tail_factorial(Result * N, N - 1).

simple_test() ->
    ?assertEqual(6, tail_factorial(3)),
    ?assertEqual(2, tail_factorial(2)),
    ?assertEqual(1, tail_factorial(1)),
    ?assertEqual(1, tail_factorial(0)).

We are using a Result variable, like we would do in a for() cycle:

for (int I = N; I > 0; I--) {
    Result = Result * I;
} // this is not Erlang code

When we call tail_factorial(3), the stack now looks like this:

factorial(6, 0) -> % now I'll return 6
factorial(6, 1) ->
factorial(3, 2) ->
factorial(1, 3) ->
factorial(3) ->

and we don't have local variables anymore. Since the return value is the same for all the functions, we can actually skip a few steps and introduce an optimization:

factorial(6, 0) -> % now I'll return 6
factorial(3) -> 

factorial(6, 0) will return directly its result to factorial(3). The tail recursive calls can overwrite the previous call to the same function in the stack, without anyone noticing.

So our main loop:

main() ->
    // wait for input, do something
    main().

now can actually work forever without exhausting the stack.

Note that for ordinary functions, with a bound number of recursive executions, tail recursion is not strictly necessary. Performance considerations must only be made after measuring two alternatives, and you shouldn't go for tail recursion just in all your functions for optimization reasons alone.

More examples, please

Let's implement the zip/2 function. It already exists in the lists module as lists:zip, but it is a good exercise for tail recursion.

This is what zip does:

    ?assertEqual([],
        zip([], [])),
    ?assertEqual([{a, 'alpha'}],
            zip([a], ['alpha'])),
    ?assertEqual([{a, 'alpha'}, {b, 'bravo'}, {c, 'charlie'}],
            zip([a, b, c], ['alpha', 'bravo', 'charlie'])).

Here's an implementation with tail recursion. We still have one accumulator variable, Result, but two arguments to pass around.

zip(First, Second) -> zip([], First, Second).

zip(Result, [], []) -> Result;
zip(Result, [FirstHead|FirstTail], [SecondHead|SecondTail]) ->
    zip(Result ++ [{FirstHead, SecondHead}], FirstTail, SecondTail).

By now you can see the pattern:

  • the original function performs initialization.
  • The function with the additional accumulator argument performs a step of iteration and relaunch itself.

Let's try a problem where the original function has to behave as a bridge. Calculating averages is tricky because you need both the size of a list and the sum of its contents.

average_test() ->
    ?assertEqual(2.0, average([2])),
    ?assertEqual(2.0, average([1, 3])),
    ?assertEqual(2.0, average([1, 3, 1, 1, 1, 5])).

Erlang has a length function, but let's build average/1 via tail recursion:

average(Numbers) ->
    {Total, Size} = average(0, 0, Numbers),
    Total / Size.

average(Total, Size, []) -> {Total, Size};
average(Total, Size, [Head|Tail]) ->
    average(Total + Head, Size + 1, Tail).

Conclusions

Tail recursion is a different way of implementing recursion - and it is optional in Erlang for implementing algorithms. However, it's mandatory when it comes to build cycles like a main/0 function.

All the code I wrote in this article is available at the Github repository for this series.

Erlang (programming language)

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Introduction to NoSQL Database
  • Leverage Lambdas for Cleaner Code
  • Isolating Noisy Neighbors in Distributed Systems: The Power of Shuffle-Sharding
  • Use AWS Controllers for Kubernetes To Deploy a Serverless Data Processing Solution With SQS, Lambda, and DynamoDB

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: