10.2.1 to avoid stack overflow with tail recursion (cont.)
[
Na?ve
It's not like English, I don't know what it means.
]
The list-handling function in the sixth chapter is not tail-recursive. If we pass a large list, it will fail because of the stack overflow. We will rewrite the two functions (map and filter) with tail recursion to correct this problem. For comparison purposes, the original implementation is included in listing 10.8. To avoid name collisions, it has been renamed MAPN and Filtern.
List 10.8 Na?ve list processing function (F #)
Na?ve ' map ' implementation
Let rec mapn F list =
Match list with
| []–> []
| X::xs-Let xs = (mapn fxs) [1]
F (x):: XS [2]
Na?ve ' filter ' implementation
Let rec filtern F list =
Match list with
| []–> []
| X::xs-Let xs = (Filtern fxs) [1]
If f (x) then X::xs else XS [2]
Each of the two functions contains a recursive call [2], but not a tail recursion, and a recursive call to each branch is followed by an additional operation [2]. The general pattern is that the function first breaks down the list into heads and tails, then recursively processes the tail and performs some actions with the head. Rather, MAPN applies the value in the end of the F function, Filtern determines whether the value in the header should be included in the result list. The final action is to append the value in the new header (or no value in the filter branch) to the end of the recursive process and must be processed after the recursive call.
To turn these functions into tail recursion, use the same accumulator parameter method that you saw earlier. When traversing a list, collect elements (filter or map) and store them in an accumulator, and once you reach the end, you can return the elements that have been collected. Listing 10.9 shows the tail-recursive implementation of the mapping and filtering.
Listing 10.9 tail recursive list processing function (F #)
Tail-recursive ' map ' implementation
Let map f list =
Let rec map ' F List acc =
Match list with
| []->list.rev (ACC) [1]
| X::xs-Let ACC =f (x):: ACC [2]
Map ' F XS ACC [3]
Map ' f list []
Tail-recursive ' filter ' implementation
Let filter f list =
Let rec filter ' F list acc=
Match list with
| []->list.rev (ACC) [1]
| X::xs-Let ACC =if f (x) then X::ACC else ACC [2]
Filter ' F XS ACC [3]
Filter ' F list []
Like the usual implementation of the tail recursive function, both functions contain local tool functions and have an additional accumulator parameter. This time, we added a single quotation mark (') to the function name, which at first looked a bit strange. F # treats single quotes as standard characters and can be used in names, so there's nothing magical about it.
We first look at the branch that terminates recursion [1], we say that we only return the collected elements, but actually first call List.rev, reverse the order of the elements. This is because the order of elements we collect is "wrong". The element added to the accumulator list is always in front and becomes the new header, so the first element that we finally process is the last element in the accumulator. Call the List.rev function to reverse the list so that the final order of the returned results is correct. This method is more efficient than what we will see in section 10.2.2, adding elements to the tail.
Now, the branch that handles the cons cell is tail-recursive. The first step deals with the elements in the header and updates the accumulator [2], generating a recursive call [3], returning the result immediately. The F # compiler can know that recursive calls are the last step and can be optimized with tail recursion.
If we paste them into F # Interactive and try to work with a large list, it's easy to see the difference between the two versions. For these functions, the recursion depth is the same as the length of the list, so if we use the Na?ve version, we will encounter problems:
> Let large = [1:100000]
val large:int list = [1; 2; 3; 4; 5; ....]
> Large |> map (fun n->n*n);;
val it:int list = [1; 4; 9; 16; 25; ....]
> Large |> mapn (fun n->n*n);;
Process is terminated due tostackoverflowexception.
It can be found that the tail recursion is an important technique for recursive processing functions. Of course, the F # Library contains a tail-recursive function that handles lists, so you don't really have to write your own mappings and filters as implemented here. In the sixth to seventh and eight chapters, we have seen that designing their own data structures, writing functions to handle them, is the key to functional programming.
Many of the data structures that will be created are quite small, but the amount of processing is quite large and the tail recursion is an important technique. With tail recursion, you can write code that can handle large datasets normally. Of course, because the function does not stack overflow, there is no guarantee that the function will complete the task within a reasonable time, which is why you need to consider the reason for more efficient processing of the list.
10.2.1 to avoid stack overflow with tail recursion (cont.)