For Loops and Folds: How we Iterate in Functional and Non-functional Languages
Let's look at a few use-cases which highlight the big three higher order functions for handling lists:
- Given a list of numbers, increase all of the numbers by one
- Given a list of numbers, return all the even numbered elements
- Given a list of numbers, return the sum of all the elements in the list
Loops
In a procedural context, these can all be solved with for loops. Let's implement in Java.
Add one to elements
public static List
Return all the even numbered elements
public static List
Return the sum of the values in the list
public static Integer
The functional approach - recursion
In pure functional languages, these problems are generally solved with a recursive approach, since this maps more to a declarative, function-oriented syntax. Let's demonstrate in Erlang.
Add one to elements
Return all the even numbered elements
Return the sum of the values in the list
Generalizing to map, filter, fold
These three examples are concrete use-cases for the "Big Three" higher order functions: map, filter, and fold. So, let's abstract out to these functions. Note that Erlang provides these functions out of the box, but for illustrative purposes, let's implement them anyway.
Add one to elements
% abstract out to map
Return all the even numbered elements
% abstract out to filter
Return the sum of the values in the list
Note that in this case, we have to provide a "starting" value upon which to fold for this to work.
Making them tail recursive
For most of these implementations, we run into the scaling problem of recursive approaches: since the frames of the previous recursive calls have to be preserved, memory usage scales linearly with the size of the given list. Languages like Erlang solve for this with Tail Call Optimization. As such, we can avoid the inefficiency as long as we make these implementations tail recursive:
Map
% Start with a 2-arity function which performs the list reverse, and provides a default starting value for the accumulator
% Use the 3-arity function with the accumulator for TCO
Filter
% Again, a 2-arity proxy for convenience
Fold
Notice that fold is already tail recursive! The last call in fold is of a call to fold, so it already enjoys tail call optimization. We can rename it to fold_tco, to match our other functions, but the syntax doesn't change at all.
Wait - don't these all look similar?
Notice that all of these tail-recursive functions follow the same form:
- They are a function of the form
function_name(List, Function, Accumulator) - In the recursive calls they make to themselves, they set the accumulator by composing the function, the head of the list, and the accumulator.
The last bit is important to see how we can refactor further.
- In tail-recursive map:
map_tco(Tail, Fun, [Fun(Head)|Acc])uses the expression[Fun(Head)|Acc] - In tail-recursive filter:
filter_tco(Tail ,Pred, [Head|Acc])uses the expression[Head|Acc](and justAccin the other tree case) - In fold:
fold_tco(Fun, Tail, Fun(Head, Accumulated))uses the expressionFun(Head, Accumulated)
Recall that fold as we originally wrote it was already tail-recursive. And in fact, if we look at the pattern we've identified, we can see that fold is in fact more abstract than map and filter. In other words, map and filter can be written to use fold! Let's try:
First map. Notice that we must reverse the list up front so that elements get ordered correctly after the fold. This can be done as a post-processing operation as well.
filter can be written this way too:
Folds and for loops
map and filter can be written in terms of fold because, conceptually, fold is doing the same work as a for loop: we start with a list, we have a "current" element in the list (we refer to it as Head since we are pattern matching against the front of the list), and we operate on we've collected so far from our previous steps through the loop, in composition with the current element. We can write a sort of fold as a for loop using the same variable names in java to demonstrate:
public static <T,E> E
By that same token, we can use the language of fold to define a for loop, but in Erlang!
Because all of our use-cases are variants on a for loop in a procedural context, they can all be similarly solved with fold.
Add One
% Or, if you'd rather append the value (less efficient in erlang, but readable), you can avoid the list reverse:
Get Evens
% again, swap append for reverse if you'd prefer here
With this longer syntax, it even starts to look a little bit like a for loop - just replace the word fold with for.
Sum
Since we're returning a single value in this case, we can avoid calling lists:reverse.
Some other use-cases
-
Find element in a list
-
Index of an element in a list
In cases like this, the accumulator doesn't really need to contain much information about the list at all. In this case, it's just a) the index to check next, and b) whether the element has been found.
Wrapping up
Whether in a functional or a procedural paradigm, programmers frequently need to operate on lists of objects. It turns out, whether you're using an iterative loop (procedural paradigm) or a tail-recursive function (functional paradigm), you conceptually have to do the same work: "seek" through the list, and operate on the current element in composition with however you've accumlated values in the previous iterations (starting, of course, with a seed element). Whether you call it a for loop or a fold, it's a remarkably similar operation.