Skip to content

Commit 7b66ed3

Browse files
authored
Add concept tail-call-recursion (#751)
* add concept * add exercise * add some practice exercises * generate exercise introj * configlet format
1 parent 955ebdd commit 7b66ed3

File tree

16 files changed

+562
-17
lines changed

16 files changed

+562
-17
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"blurb": "Learn how to optimize tail call recursion in Elm",
3+
"authors": [
4+
"jiegillet"
5+
],
6+
"contributors": []
7+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# About
2+
3+
A function is [tail-recursive][recursion-tc] if the _last_ thing executed by the function is a call to itself.
4+
5+
Each time any function is called, a _stack frame_ with its local variables and arguments is put on top of [the function call stack][call-stack].
6+
When a function returns, the stack frame is removed from the stack.
7+
8+
Tail-recursive functions allow for [tail call optimization][tail-call-optimization] (or tail call elimination).
9+
It's an optimization that allows reusing the last stack frame by the next function call when the previous function is guaranteed not to need it anymore.
10+
This mitigates concerns of overflowing the function call stack, which is a situation when there are so many frames on the function call stack that there isn't any memory left to create another one.
11+
12+
## Tail Call Optimization in Elm
13+
14+
Under some condition, the Elm compiler is able to automatically performs an a tail call optimization when compiling to JavaScript.
15+
16+
The optimization can happen for a recursive function when the _last_ operation in a branch is done by calling the function itself in a simple function application.
17+
Let's look at some examples:
18+
19+
```elm
20+
factorial : Int -> Int
21+
factorial n =
22+
if n <= 1 then
23+
n
24+
else
25+
n * factorial (n-1)
26+
```
27+
28+
The implementation above is not tail recursive, because the last operation in the `else` branch is a multiplication `n *`.
29+
30+
```elm
31+
factorial : Int -> Int
32+
factorial n =
33+
factorialHelper n n
34+
35+
factorialHelper : Int -> Int -> Int
36+
factorialHelper n resultSoFar =
37+
if n <= 1 then
38+
resultSoFar
39+
else
40+
factorialHelper (n-1) (n * resultSoFar)
41+
```
42+
43+
The implementation above is tail recursive and will be optimized, because the last operation in the `else` branch is `factorialHelper` calling itself.
44+
This would not be possible for a function with the type signature `Int -> Int`, and in practice tail call optimization is often achieved by defining helper functions.
45+
46+
## Edge cases
47+
48+
In some cases, the compiler is not able to optimize recursive functions, let's look at some examples.
49+
50+
While `f x = f (x - 1)` is tail call recursive, `f x = (x - 1) |> f` is not, because the function application `|>` is the last operation and is considered separate from `f`.
51+
Similarly, functions ending with boolean conditions `f x = x > 0 || f (x - 1)` are not tail recursive.
52+
53+
The function `f x = if f (x - 1) then 1 else 0` is not tail recursive, because the expression in the `if` is not considered to be a branch.
54+
Similarly, a recursive call in a `case` statement would not be optimized either.
55+
56+
The function `f x = let y = f (x - 1) in y`, is not tail recursive because it calls itself in a `let` statement, which is not considered to be a branch.
57+
However, in `f x = let g x = g (x - 1) in g x` the function `g` it tail recursive and will be optimized even though it's defined in a `let` statement.
58+
59+
The function `f x = if x > 0 then f (x + 1) else 1 + f x` will be _partially_ optimized: the `then` branch can be optimized but the `else` branch cannot.
60+
Partial optimization is the best that can be achieved in some situations, such as traversing tree-like structures or defining co-recursive functions.
61+
62+
[recursion-tc]: https://en.wikipedia.org/wiki/Tail_call
63+
[call-stack]: https://en.wikipedia.org/wiki/Call_stack
64+
[tail-call-optimization]: https://jfmengels.net/tail-call-optimization/
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Introduction
2+
3+
A function is [tail-recursive][recursion-tc] if the _last_ thing executed by the function is a call to itself.
4+
5+
Each time any function is called, a _stack frame_ with its local variables and arguments is put on top of [the function call stack][call-stack].
6+
When a function returns, the stack frame is removed from the stack.
7+
8+
Tail-recursive functions allow for [tail call optimization][tail-call-optimization] (or tail call elimination).
9+
It's an optimization that allows reusing the last stack frame by the next function call when the previous function is guaranteed not to need it anymore.
10+
This mitigates concerns of overflowing the function call stack, which is a situation when there are so many frames on the function call stack that there isn't any memory left to create another one.
11+
12+
## Tail Call Optimization in Elm
13+
14+
Under some condition, the Elm compiler is able to automatically performs an a tail call optimization when compiling to JavaScript.
15+
16+
The optimization can happen for a recursive function when the _last_ operation in a branch is done by calling the function itself in a simple function application.
17+
Let's look at some examples:
18+
19+
```elm
20+
factorial : Int -> Int
21+
factorial n =
22+
if n <= 1 then
23+
n
24+
else
25+
n * factorial (n-1)
26+
```
27+
28+
The implementation above is not tail recursive, because the last operation in the `else` branch is a multiplication `n *`.
29+
30+
```elm
31+
factorial : Int -> Int
32+
factorial n =
33+
factorialHelper n n
34+
35+
factorialHelper : Int -> Int -> Int
36+
factorialHelper n resultSoFar =
37+
if n <= 1 then
38+
resultSoFar
39+
else
40+
factorialHelper (n-1) (n * resultSoFar)
41+
```
42+
43+
The implementation above is tail recursive and will be optimized, because the last operation in the `else` branch is `factorialHelper` calling itself.
44+
This would not be possible for a function with the type signature `Int -> Int`, and in practice tail call optimization is often achieved by defining helper functions.
45+
46+
[recursion-tc]: https://en.wikipedia.org/wiki/Tail_call
47+
[call-stack]: https://en.wikipedia.org/wiki/Call_stack
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"url": "https://functional-programming-in-elm.netlify.app/recursion/tail-call-elimination.html",
4+
"description": "Tail-Call Elimination explained by the creator of Elm"
5+
},
6+
{
7+
"url": "https://jfmengels.net/tail-call-optimization/",
8+
"description": "Detailed explanation of tail-call optimization in Elm"
9+
}
10+
]

config.json

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,20 @@
416416
"pattern-matching"
417417
],
418418
"status": "beta"
419+
},
420+
{
421+
"slug": "pipers-pie",
422+
"name": "Piper's Pie",
423+
"uuid": "1f54e219-3f76-4df8-996a-9308ce9d6458",
424+
"concepts": [
425+
"tail-call-recursion"
426+
],
427+
"prerequisites": [
428+
"let",
429+
"recursion",
430+
"basics-2"
431+
],
432+
"status": "beta"
419433
}
420434
],
421435
"practice": [
@@ -881,9 +895,11 @@
881895
"name": "Collatz Conjecture",
882896
"uuid": "0edd5fa2-f1db-4572-9810-55a2c6729753",
883897
"practices": [
884-
"result"
898+
"tail-call-recursion"
885899
],
886900
"prerequisites": [
901+
"recursion",
902+
"tail-call-recursion",
887903
"result",
888904
"booleans"
889905
],
@@ -894,11 +910,12 @@
894910
"name": "Binary Search",
895911
"uuid": "5c859d35-f2fe-455c-a7d7-df9398355403",
896912
"practices": [
897-
"comparison",
913+
"tail-call-recursion",
898914
"array"
899915
],
900916
"prerequisites": [
901917
"comparison",
918+
"tail-call-recursion",
902919
"array",
903920
"maybe"
904921
],
@@ -929,17 +946,16 @@
929946
"name": "Pascal's Triangle",
930947
"uuid": "b7e456d7-e383-4e03-9126-c9b57c1287e1",
931948
"practices": [
932-
"lists"
949+
"tail-call-recursion"
933950
],
934951
"prerequisites": [
935952
"lists",
936953
"booleans",
937-
"let"
954+
"let",
955+
"recursion",
956+
"tail-call-recursion"
938957
],
939-
"difficulty": 4,
940-
"topics": [
941-
"recursion"
942-
]
958+
"difficulty": 4
943959
},
944960
{
945961
"slug": "roman-numerals",
@@ -1750,6 +1766,11 @@
17501766
"uuid": "651a2eba-41d2-4b80-a861-7291536cad5c",
17511767
"slug": "recursion",
17521768
"name": "Recursion"
1769+
},
1770+
{
1771+
"uuid": "edf8ab39-7e08-4dec-ae86-f1a14112e022",
1772+
"slug": "tail-call-recursion",
1773+
"name": "Tail Call Recursion"
17531774
}
17541775
],
17551776
"key_features": [
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Hints
2+
3+
## General
4+
5+
- You can read about [Tail Call Elimination][tail-call-elimination] from the creator of Elm
6+
- In tail recursive functions, the _last_ operation in a branch calls itself in a simple function application
7+
- You will need to define tail recursive helper functions
8+
9+
## 1. Factorial
10+
11+
- You will need to define a tail recursive helper function
12+
- The factorial of 0 is 1
13+
- Use recursion to reduce `n` to its base case, while keeping track of the product
14+
15+
## 2. Double Factorial
16+
17+
- You will need to define a tail recursive helper function
18+
- The double factorial of 0 is 1
19+
- The double factorial of 1 is 1
20+
- Use recursion to reduce `n`, two by two, to its base case, while keeping track of the product
21+
22+
## 3. Newton/Euler Convergence Transformation
23+
24+
- You will need to define a tail recursive helper function
25+
- Using `factorial` and `doubleFactorial` directly is discouraged, as it is not the most efficient way to create the terms of the sum
26+
- You can pass as many arguments as you wish to the helper function
27+
28+
[tail-call-elimination]: https://functional-programming-in-elm.netlify.app/recursion/tail-call-elimination.html
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Instructions
2+
3+
Piper is an avid pie baker.
4+
5+
No one knows if she picked pie baking because of her name, or if she changed her name to match her hobby.
6+
At a glance, the latter doesn't seem very likely, but you see, Piper is absolutely fascinated by pies.
7+
She's always tinkering in the kitchen, tweaking her recipes, improving her craft, to the absolute delight of her friends.
8+
9+
Her latest interest?
10+
Baking pies as circular as possible, to the point of mathematical perfection, with the help of her favorite number, you guessed it: π.
11+
12+
Piper found a delightful formula to calculate π iteratively, the Newton/Euler Convergence Transformation:
13+
14+
```
15+
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
16+
```
17+
18+
Help Piper bake her mathematically perfect pie by calculating π.
19+
20+
## 1. Factorial
21+
22+
Let's warm up first.
23+
The factorial operator, usually written `!`, is defined as
24+
25+
```
26+
0! = 1
27+
n! = 1 * 2 * 3 * ... * n
28+
```
29+
30+
Define the `factorial` function which will compute the factorial in a tail recursive manner.
31+
32+
```elm
33+
factorial 4
34+
-- 24
35+
```
36+
37+
## 2. Double Factorial
38+
39+
The double factorial operator, usually written `!!`, is defined as
40+
41+
```
42+
0!! = 1
43+
n!! = 1 * 3 * 5 * ... * n (for odd n)
44+
n!! = 2 * 4 * 6 * ... * n (for even n)
45+
```
46+
47+
Define the `doubleFactorial` function which will compute the factorial in a tail recursive manner.
48+
49+
```elm
50+
factorial 5
51+
-- 15
52+
factorial 6
53+
-- 48
54+
```
55+
56+
## 3. Newton/Euler Convergence Transformation
57+
58+
Define the `pipersPi` function, which will approximate π using a set number of terms from the Newton/Euler Convergence Transformation formula in a tail recursive manner.
59+
60+
```
61+
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
62+
```
63+
64+
Let's compute the first term together.
65+
For an upper limit of `0` (instead of infinity), we get:
66+
67+
```
68+
π / 2 ≈ Sum for k from 0 to 0 of ( k! ) / ( 2 * k + 1 )!!
69+
π / 2 ≈ ( 0! ) / ( 2 * 0 + 1 )!!
70+
π / 2 ≈ 0! / 0!!
71+
π / 2 ≈ 1 / 1
72+
π / 2 ≈ 1
73+
π ≈ 2
74+
```
75+
76+
Each extra term will improve the approximation.
77+
78+
```elm
79+
pipersPi 0
80+
-- 2.0
81+
pipersPi 1
82+
-- 2.6666666
83+
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Introduction
2+
3+
## Tail Call Recursion
4+
5+
A function is [tail-recursive][recursion-tc] if the _last_ thing executed by the function is a call to itself.
6+
7+
Each time any function is called, a _stack frame_ with its local variables and arguments is put on top of [the function call stack][call-stack].
8+
When a function returns, the stack frame is removed from the stack.
9+
10+
Tail-recursive functions allow for [tail call optimization][tail-call-optimization] (or tail call elimination).
11+
It's an optimization that allows reusing the last stack frame by the next function call when the previous function is guaranteed not to need it anymore.
12+
This mitigates concerns of overflowing the function call stack, which is a situation when there are so many frames on the function call stack that there isn't any memory left to create another one.
13+
14+
### Tail Call Optimization in Elm
15+
16+
Under some condition, the Elm compiler is able to automatically performs an a tail call optimization when compiling to JavaScript.
17+
18+
The optimization can happen for a recursive function when the _last_ operation in a branch is done by calling the function itself in a simple function application.
19+
Let's look at some examples:
20+
21+
```elm
22+
factorial : Int -> Int
23+
factorial n =
24+
if n <= 1 then
25+
n
26+
else
27+
n * factorial (n-1)
28+
```
29+
30+
The implementation above is not tail recursive, because the last operation in the `else` branch is a multiplication `n *`.
31+
32+
```elm
33+
factorial : Int -> Int
34+
factorial n =
35+
factorialHelper n n
36+
37+
factorialHelper : Int -> Int -> Int
38+
factorialHelper n resultSoFar =
39+
if n <= 1 then
40+
resultSoFar
41+
else
42+
factorialHelper (n-1) (n * resultSoFar)
43+
```
44+
45+
The implementation above is tail recursive and will be optimized, because the last operation in the `else` branch is `factorialHelper` calling itself.
46+
This would not be possible for a function with the type signature `Int -> Int`, and in practice tail call optimization is often achieved by defining helper functions.
47+
48+
[recursion-tc]: https://en.wikipedia.org/wiki/Tail_call
49+
[call-stack]: https://en.wikipedia.org/wiki/Call_stack
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Introduction
2+
3+
%{concept: tail-call-recursion}

0 commit comments

Comments
 (0)