Skip to content

Commit

Permalink
added new notebook
Browse files Browse the repository at this point in the history
  • Loading branch information
Jim Marshall committed Sep 13, 2018
1 parent bfca8aa commit 8f89a72
Showing 1 changed file with 213 additions and 0 deletions.
213 changes: 213 additions & 0 deletions notebooks/slc_2018/Thinking_Recursively_in_Scheme.ipynb
@@ -0,0 +1,213 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Thinking Recursively in Scheme\n",
"\n",
"Scheme is a great programming language for learning to think recursively. When you first encounter recursion, it can feel a lot like magic. This can be a good thing or a bad thing, depending on your point of view. If you're the kind of person who loves watching magic shows, who loves the feeling of being \"taken in\" by the illusion, you'll probably immediately enjoy writing recursive programs. On the other hand, if you're the kind of person who wants to know what's really going on behind the scenes (\"how did they _do that_?!\"), who feels a nagging sense of unease after seeing a really convincing magic trick unfold before your eyes because you can't figure out how it was done...well, you may initially find recursion to be a bit perplexing and frustrating. But fear not! I, too, was in the latter category when I first encountered recursion, way back when. But recursion has a way of growing on you over time, and I predict that with a little perseverance, you too will soon come to appreciate its many magical charms.\n",
"\n",
"One of the paradoxical qualities that often makes learning to write recursive programs so hard is that writing recursive programs is so _easy_ (once you get the hang of it). But getting the hang of it usually takes a lot of practice, because you really do have to learn to _think_ in a new and unfamiliar way. The good news is that if you keep the following rules of thumb in mind whenever you are faced with the task of writing a recursive program, it will make the task much easier. So here we go..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### The Base Case\n",
"\n",
"The first thing you always have to do when writing a recursive program is to ask yourself: what is the _base case_ for this program? The \"base case\" is the simplest form of input data that your program will ever have to deal with, and the details of what that means will depend on exactly what your program is supposed to do. Sometimes a program may even have multiple base cases.\n",
"\n",
"Suppose we wish to write a Scheme function called <tt>addup</tt> that takes in a list of numbers and adds them all up. The list might contain anywhere from a single number to a million numbers or more, or possibly no numbers at all. For <tt>addup</tt>, the \"simplest form of input data\" would just be the empty list. No thinking at all is required to figure out the sum of an empty list, because the answer is obviously 0. So here is a preliminary sketch of our function definition for <tt>addup</tt>:\n",
"\n",
"```scheme\n",
"(define addup\n",
" (lambda (nums)\n",
" (cond\n",
" ((null? nums) 0)\n",
" ...\n",
"```\n",
"\n",
"Notice how we use <tt>(null? nums)</tt> to check for the empty list. This is the appropriate base-case condition for <tt>addup</tt>. In general, however, the correct base-case condition for a function will depend on the type of data that the function expects, as well as what it is supposed to compute. For other recursive functions that take a list as input, it might be more appropriate to check if the list has exactly one element, rather than no elements. For functions that operate on numbers, the base case might be when the input is equal to zero."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### The General Case\n",
"\n",
"Our function definition is still incomplete, but it should be clear that the code sketched above will give the right answer for our base case. So far so good. Next, we need to consider how our function should handle the _general case_ &#151; that is, input data other than the base case. For <tt>addup</tt>, that means any list containing at least one number. For example, the list <tt>(1 2 3 4 5 6)</tt>. Or maybe a list containing a million numbers.\n",
"\n",
"Now we come to the magic. Even though we're not yet finished with our function definition, we're going to make a crazy assumption. We're going to simply pretend that <tt>addup</tt> somehow _already works correctly_ on _any_ list that we give it, and we're going to _use that assumption_ to help us complete the function definition. For example, suppose we call <tt>addup</tt> on the input list <tt>(1 2 3 4 5 6)</tt>. This list is not empty, so the base-case clause of the <tt>cond</tt> gets skipped. How then do we figure out the sum of all the numbers? Answer: we will use <tt>addup</tt> itself to help us out! We first make our list slightly shorter by calling <tt>(cdr nums)</tt>, which gives us the list <tt>(2 3 4 5 6)</tt>. Then we call <tt>addup</tt> recursively on that shorter list, which we just _assume_ will somehow magically give us 20, since 20 is the sum of the shorter list <tt>(2 3 4 5 6)</tt>. But we're not quite finished yet, because we still need to add the <i>first</i> number in the original list, namely the 1 (which we temporarily dropped before calling the recursion), to this sum to get the correct answer for the _whole_ list. Here is the complete recursive definition:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"(define addup\n",
" (lambda (nums)\n",
" (cond\n",
" ((null? nums) 0)\n",
" (else (+ (car nums) (addup (cdr nums)))))))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Will this really work? Let's try it out!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"(addup '(1 2 3 4 5 6))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"(addup '(10 6 3 9 18 5 2 2 7 15 4 6 9 8 24 42 77 0 1 32))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In the first example, <tt>nums</tt> is the list <tt>(1 2 3 4 5 6)</tt>, <tt>(car nums)</tt> is <tt>1</tt>, <tt>(cdr nums)</tt> is the list <tt>(2 3 4 5 6)</tt>, and <tt>(addup (cdr nums))</tt> is <tt>20</tt>. In the second example, <tt>(car nums)</tt> is <tt>10</tt>, and <tt>(addup (cdr nums))</tt> gives <tt>270</tt>, which is the sum of all of the remaining numbers in the list. This then gets added to the <tt>10</tt> to give the final answer of <tt>280</tt> for the entire list.\n",
"\n",
"How can we be sure that <tt>(addup (cdr nums))</tt> really will give us the correct answer for the shorter list in all cases? After all, we just magically _assumed_ that it will. The beauty of recursion rests on the principle of _mathematical induction_, which guarantees that your program will work correctly in all cases as long as the following three things are true:\n",
"\n",
"1. Your program gives the correct answer for the base case.\n",
"\n",
"2. Your program only calls itself recursively on _simpler_ versions of the input data.\n",
"\n",
"3. Your program uses the answer given by the recursion to construct an appropriate answer for the original input data.\n",
"\n",
"In our example, <tt>addup</tt> calls itself recursively on <tt>(cdr nums)</tt>, which is \"simpler\" than <tt>nums</tt> because it is shorter by one number. Even if <tt>nums</tt> contains a million numbers, <tt>(cdr nums)</tt> will be slightly shorter, and thus slightly \"closer\" to the base case. Furthermore, <tt>addup</tt> adds the first number in the list to the sum computed by the recursion, in order to form the correct answer for the whole list. This is typical of recursive functions: after calling themselves recursively, they usually need to do a little more \"work\" before returning their final answer. But the bulk of the work &#151; such as figuring out the sum of the <tt>cdr</tt> of the list, even if the <tt>cdr</tt> contains thousands or millions of numbers &#151; is handled \"magically\" by the recursive call."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Another Example\n",
"\n",
"Let's try a slightly more sophisticated example. This time we will write a function called <tt>smallest</tt> that takes in a list of numbers and returns the smallest one, instead of adding them all up. The list might contain anywhere from a single number to a million numbers or more. For this example, let's stipulate that the list will never be empty, because asking for the smallest number of an empty list doesn't make any sense. Here the \"simplest form of input data\" would be a list containing just a single number, such as <tt>(42)</tt>. No thinking at all is required to figure out which of the numbers in the list is the smallest, because there's only one number. So we can just return that number, whatever it is, and we're done. Note that we need to return the number itself, not the list, as our answer, because the job of <tt>smallest</tt> is to return the smallest _number_ from the list. In other words, we need to use the <tt>car</tt> operation to retrieve the number from the list. But that's easy enough to do. So here is a preliminary sketch of our function definition for <tt>smallest</tt>:\n",
"\n",
"```scheme\n",
"(define smallest\n",
" (lambda (nums)\n",
" (cond\n",
" ((null? (cdr nums)) (car nums))\n",
" ...\n",
"```\n",
"\n",
"Notice how we use <tt>(null? (cdr nums))</tt> to check if the list contains exactly one number. This is the appropriate base-case condition for <tt>smallest</tt>. Although our function definition is still incomplete, it should be clear that the code sketched above will give the right answer for each of the following input lists (and indeed, for any list containing a single number):\n",
"\n",
"```scheme\n",
"(smallest '(42)) => 42\n",
"(smallest '(-5)) => -5\n",
"(smallest '(1000)) => 1000\n",
"```\n",
"\n",
"So far so good. Now, what about the general case? For <tt>smallest</tt>, that means any list containing more than one number. For example, the list <tt>(9 5 10 3 18)</tt>. Or maybe a list containing a million numbers, with the smallest number buried somewhere far down in the middle or toward the end of the list.\n",
"\n",
"Once again, it's time for some magic. Even though we're not yet finished with our function definition, let's pretend that <tt>smallest</tt> somehow _already works correctly_ on _any_ list that we give it, and let's _use that assumption_ to help us complete the function definition. For example, suppose we call <tt>smallest</tt> on the input list <tt>(9 5 10 3 18)</tt>. This list contains more than one number (that is, its <tt>cdr</tt> is not null), so the base-case clause of the <tt>cond</tt> is skipped. How then do we retrieve the smallest number from the list? Answer: just like before, we will use the function _recursively_ to help us out! We first make our list slightly shorter by calling <tt>(cdr nums)</tt>, which gives us <tt>(5 10 3 18)</tt>. Then we call <tt>smallest</tt> recursively on that shorter list, which we just _assume_ will somehow magically give us 3, since 3 is the smallest number in the shorter list <tt>(5 10 3 18)</tt>. But we're not quite finished yet, because we still need to compare the 3 to the <i>first</i> number in the original list, namely the 9 (which we temporarily dropped before calling the recursion), to make sure that the 3 really is the smallest number in the _whole_ list. In this case, the 3 is in fact smaller than the 9, so we return the 3 as our final answer, and we're done. On the other hand, if the original list had been <tt>(2 5 10 3 18)</tt>, the first number (the 2) would be _smaller_ than the 3, and so we would need to return the 2 instead of the 3. Here is the complete recursive definition:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"(define smallest\n",
" (lambda (nums)\n",
" (cond\n",
" ((null? (cdr nums)) (car nums))\n",
" ((< (car nums) (smallest (cdr nums))) (car nums))\n",
" (else (smallest (cdr nums))))))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Will this really work? Let's try it out!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"(smallest '(9 5 10 3 18))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"(smallest '(2 5 10 3 18))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"(smallest '(7 4 2 13 9 1 8 9 15 3))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To summarize: in the first example, <tt>nums</tt> is the list <tt>(9 5 10 3 18)</tt>, <tt>(cdr nums)</tt> is the list <tt>(5 10 3 18)</tt>, <tt>(car nums)</tt> is <tt>9</tt>, and the recursive call <tt>(smallest (cdr nums))</tt> gives <tt>3</tt>, so <tt>3</tt> gets returned as the final answer because <tt>3</tt> is smaller than <tt>9</tt>. The second example is similar, except that <tt>(car nums)</tt> is now <tt>2</tt>, which is smaller than <tt>3</tt>, so the <tt>2</tt> gets returned instead of the <tt>3</tt>. In the third example, <tt>(smallest (cdr nums))</tt> gives <tt>1</tt>, which is smaller than the <tt>7</tt> at the front of the list, so the <tt>1</tt> is returned instead of the <tt>7</tt>.\n",
"\n",
"Just like <tt>addup</tt>, <tt>smallest</tt> calls itself recursively on <tt>(cdr nums)</tt>, which is \"simpler\" than <tt>nums</tt> because it is shorter by one number, and thus slightly \"closer\" to the base case. This recursive call performs the bulk of the work: namely, figuring out the smallest number in the <tt>cdr</tt> of the list. Even if the <tt>cdr</tt> contains thousands or millions of numbers, the recursive call will \"magically\" give the right answer for the <tt>cdr</tt>. Unlike <tt>addup</tt>, however, the \"additional work\" done by <tt>smallest</tt> is to _compare_ the number returned by the recursion to the first number in the list, instead of adding them together, in order to determine the correct answer for the whole list. It doesn't just blindly return <tt>(smallest (cdr nums))</tt> in every case, because that won't be the correct answer if <tt>(car nums)</tt> happens to be the smallest number in the whole list. An additional decision is necessary to ensure that <tt>smallest</tt> works for every list.\n",
"\n",
"### An Important Caveat\n",
"\n",
"The above examples illustrate the right way to approach thinking about recursion. Understanding how a recursive function works is easiest if you think about it _on just the top level_. In other words, like we did above, just assume that the recursion will magically give you the right answer, and don't worry about what actually happens \"inside\" the recursive call &#151; provided that you always call it on \"simpler\" versions of the input data. If your function explicitly handles the base case correctly, and uses the results of the recursive call in the right way to construct an appropriate answer for the original input data, mathematical induction will see to it that your function works correctly in all cases.\n",
"\n",
"That said, even perfectly correct recursive code can sometimes lead to severe inefficiencies in execution speed. For example, if you played around with <tt>smallest</tt>, you may have noticed a dramatic slowdown in speed on lists that are only slightly longer than the examples given above. The reason for this is not hard to see. Suppose we call <tt>smallest</tt> on the list <tt>(14 13 12 11 10 9 8 7 6 5 4 3 2 1)</tt>. The recursive call <tt>(smallest (cdr nums))</tt> gives 1. This result is compared to <tt>(car nums)</tt>, which is not less than <tt>1</tt>, so the <tt>else</tt> line of the conditional gets executed next. This generates a second &#151; identical &#151; recursive call to <tt>(smallest (cdr nums))</tt>, which just re-computes the <tt>1</tt> all over again. This redundant computation happens on every level of the recursion, all the way down to the base case, leading to an exponential slowdown in speed.\n",
"\n",
"Fortunately, this problem is very easy to fix, but requires rewriting the code in a slightly different way using a feature of Scheme that I will introduce a little later on in the semester. For now, the important thing is to learn to think recursively. We won't worry too much about efficiency at this point, because it is often easy to transform a correct but inefficient program into a much more efficient one by applying some simple optimizations."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Calysto Scheme 3",
"language": "scheme",
"name": "calysto_scheme"
},
"language_info": {
"codemirror_mode": {
"name": "scheme"
},
"mimetype": "text/x-scheme",
"name": "scheme",
"pygments_lexer": "scheme"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

0 comments on commit 8f89a72

Please sign in to comment.