Problems in Comprehending Recursion and Suggested ... - CiteSeerX

21 downloads 42346 Views 34KB Size Report
Department of Computer Science ... beginning programmers consider recursion to be a difficult .... In a class setting, sigma is a good example to start off with. In.
Proceedings of the 6th annual conference on Innovation and Technology in Computer Science Education (ITiCSE), Canterbury, UK, June 2001, pp 25—28.

Problems in Comprehending Recursion and Suggested Solutions Raja Sooriamurthi Department of Computer Science University of West Florida Pensacola, FL, 32514, USA

[email protected] ABSTRACT Recursion is a very powerful and useful problem solving strategy. But, along with pointers and dynamic data structures, many beginning programmers consider recursion to be a difficult concept to master. This paper reports on a study of upper-division undergraduate students on their difficulty in comprehending the ideas behind recursion. Three issues emerged as the points of difficulty for the students: (1) insufficient exposure to declarative thinking in a programming context (2) inadequate appreciation of the concept of functional abstraction (3) lack of a proper methodology to express a recursive solution. The paper concludes with a discussion of our approach to teaching recursion, which addresses these issues. Classroom experience indicates this approach effectively aids students’ comprehension of recursion.

1. BACKGROUND As an alternative to a survey of languages, our programming languages course involves the study of language implementation issues — a hands-on approach to under the hood issues. All upper-division undergraduate students majoring in computer science take this course. The programming component of the course involves a cascaded series of assignments developing an interpreter for a sub-set of the language Scheme[5] in C. The interpreter is developed in 5 parts: (1) scanner (2) reader and printer (3) basic evaluator (4) evaluator supporting first class functions (closures) and (5) mark and sweep garbage collector. While many of the algorithms involved are naturally recursive (e.g., the list printer) students have the alternative of implementing supplementary and library routines (such as those needed to manage the heap) either iteratively or recursively. All students in the class are post data-structures students with an intermediate skill level of programming. Yet many have problems in expressing recursive solutions. We carried out a study of students who had problems developing recursive code. On a one on one basis students were encouraged to think aloud during their problem solving process. From these problem solving voice protocols the following issues arose as to why students were having difficulty expressing recursive solutions:



Insufficient exposure to declarative thinking in a programming context. • Inadequate appreciation of the concept of functional abstraction • Lack of a proper methodology to express a recursive solution. The remainder of the paper discusses these issues in further detail and follows up with a pedagogical strategy we have found useful to teach recursion.

2. MANAGING COMPLEXITY Programming is about a new way of thinking and problem solving — the algorithmic way. In this, programming is more about designing and building problem solvers rather than the syntax and idiosyncrasies of a particular language. As with any design task a key issue is mastering the inherent complexity. While complexity cannot be avoided the best we can do is to learn how to cope with it. Two tools that students are taught to help slay the dragon of complexity are (1) abstraction, which helps to reduce it and (2) modularity, which helps to better manage it [2,8].

2.1 Abstraction and Declarative Thinking Functional abstraction is a cornerstone strategy in good software design. To understand functional abstraction one needs to be comfortable in thinking about what a function does as opposed to how it does it. To master recursion is to master and acquire a fundamental understanding of functional abstraction. A fundamental tenet of abstraction is that the way something works need not necessarily be the way we have to work with it. When learning to drive a car there are two forms of knowledge: (1) knowing how to operate a car (2) knowing how the car operates. Naturally one can be a very good driver without having much knowledge of how a car itself operates. This is exactly the same emphasis needed when teaching abstraction in general and recursion in particular. The issue is simply the separation of concerns: the separation of what needs to be done from how it will be done. We observed that students normally have a hard time comprehending recursion because they don't clearly differentiate between these two forms of knowledge (the what vs. the how) and worse, tend to focus on the latter — the how. The key to comprehending any form of abstraction including recursion is to focus on the what and down play the how. Interestingly, in a declarative context such as that of mathematics, students seem to be comfortable in applying recursive concepts.

For example, consider the standard problem of summing a sequence of integers. Such a sum could be expressed mathematically in a number of ways. The most direct way would be:

In the spirit of the Wall Street dictum, “buy low and sell high”, the above just specifies a policy without giving any details of how to do the splitting nor how to combine the sub-solutions. It says what needs to be done rather than how to do it.

n

∑i

= m + (m+1) + … + (n-1) + n

i =m

Students are very comfortable with the above. When the sum is mathematically expressed in several different recursive ways most students continue to feel comfortable: n −1

n

∑i

=

i =m

∑i

+

m

+

n

i =m

n

=

∑i

i = m +1 p

=

∑i

n

+

i =m

∑i

i = p +1

But, when the same sum is recursively expressed as function in a programming language it suddenly takes on a very mysterious aura: (For illustrative purposes a C style syntax has been used. Naturally the issues under discussion are independent of the language being used.) int sigma (int m, int n) { if (m>n) return 0; else return m + sigma(m+1,n); } In the world of mathematics students are normally concerned about the declarative what part and not so much (if at all) on an imperative how part. Similar to [4], we found that in a curriculum taught using an imperative language, imperative thinking takes on an importance of its own. By the time recursion is taught imperative thinking has been so strongly indoctrinated that the declarative thinking needed for abstraction is rarely exercised. As a first step in understanding the above recursive routine students need to recognize that: n

∑i



sigma(m, n)

3. PROBLEM TYPE, INSTANCE AND SIZE When learning recursion it helps to make explicit the notion of a problem type and problem size. Rather than a formal definition of what a problem type is, relying on student intuitions helps best. Finding the roots of a polynomial, finding the largest number in a collection, sorting a collection of numbers, multiplying two matrices, traversing a maze are all examples of problem types. For each type of a problem there are usually countless number of problem instances. A specific polynomial, a particular maze layout are examples of instances of their corresponding problem type. Associated with each problem instance there is a measure of problem size. For example, for matrix multiplication the dimensions of the matrix; for sorting the number of numbers or items in general (their individual magnitudes do not matter); for determining primality the magnitude of the number etc. In further cultivating student intuitions we have found that it helps to illustrate these concepts by viewing problem types as geometric figures. The problem size would then be reflected as the size of the geometric figures. To illustrate, consider the problem of buying a car. Figuratively this could be represented as a rounded rectangle as given in Figure-1. This problem could be split into two (a) find a car that one likes (an oval) and (b) arrange financing (a rectangle). buy abcar

find a car

arrange financing

Figure 1. An illustration of the divide and conquer strategy: dividing a rounded rectangle problem into two problems of an oval and a rectangle. An important issue to make explicit here is that a large problem is being split into two sub-problems each possibly of different types (figuratively emphasized as an oval and a rectangle). The divide and conquer strategy says that in order to solve the problem of buying a car we need to solve the sub-problems of finding a car, arranging financing and then combine their individual solutions.

i =m

are just different notations for the same declarative idea.

2.2 Modularity: Divide & Conquer Modularity is achieved with the simple idea of divide and conquer, which is just a manifestation of the age-old tactic of division of labor. Essentially the divide and conquer problem solving strategy says: 1. 2. 3.

Divide a large problem into smaller pieces. Solve the smaller pieces. Combine the solutions.

One of the key steps towards comprehending recursion is to emphasize that it is just a special case of the divide and conquer strategy where the individual sub-problems are of the same type. Figuratively, to solve a “square” we split it not into a smaller “circle'” and a smaller “rectangle'” but into smaller “squares” themself (Figure-2):

4.1 An Example

p P0

In a class setting, sigma is a good example to start off with. In this paper let us consider an alternative problem for which students normally see an iterative solution first: the problem of summing the individual digits of an integer: sumDigits(4296) = 4 + 2 + 9 + 6 = 21

P1

P2

Figure 2 Recursion is a special case of divide and conquer where the original problem (p0) is split into smaller problems (p1 & p2) of the same type.

4. THE TEMPLATE As discussed in the earlier section, the key to understanding any abstraction in general and recursion in particular is to deemphasize the how part of a computation and to emphasize the what part. In that vein, we have found it useful to emphasize what one needs to do to use recursion and de-emphasize how recursion actually works. Our students who have problems conceptualizing a recursive solution have been aided by judiciously following the below template. Each stage of the template is tagged and students are guided through the process of filling it out with various examples: ½

Base case: the smallest instance (in terms of problem size) of the problem for which we know the answer immediately without any effort. We need two things for this: • •

a test to recognize the base case and an answer for the base case

½

Simplifying routine: something that helps us “shrink'” the problem size towards the base case.

½

Natural recursion: mechanically express a recursive call of the function to the simplified version of the problem, i.e., to the problem that arises after the simplifying routine has been applied.

½

Completion: This is the key step, which is usually the stumbling block in coming to terms with recursion. Here is where functional abstraction needs to be exercised: Without thinking about how the recursive call would work we need to think about what its answer should be. Given that answer, one needs to ask: what needs to be done to the answer of the recursive call to form the answer to the original problem we started off with.

The importance and use of explicitly labeling the various stages of a solution to a problem has been an issue brought to the forefront of design by the patterns movement in software development [3]. It is very important for students to explicitly think in terms of these labeled steps and work through the requirements of the template step by step. Having the students explicitly vocalize their reasoning through each of the labeled stages helps to trace their logic and to localize the source of their errors and misconceptions. The following section discusses our use of this template with an example.

Base case: On a little reflection it is not too hard to identify that the smallest integers for which it is trivial to solve the problem are single digit integers. o a test n < 10 o an answer n

Simplifying routine: This is a little bit tricky in this problem. A novice may initially be tempted to say: reduce n by 1, as this will move us towards the base case. The way to avoid such errors is to recall that recursion is a special case of divide and conquer where the sub-problems need to be the same type as the original problem but of lesser size. So students need to identify what is the problem type and what is the measure of problem size. On some more reflection students realize that it is not the magnitude of the individual numbers but rather the number of digits they contain that constitutes the problem size. Hence the problem needs to be split up in such a way that we end up with subproblems of the same type, i.e., finding the sum of digits in an integer. Conceptually there are several ways this could be done for the above given example: 4 42 429

296 96 6

= = =

4 + (4+2) + (4+2+9) +

(2+9+6) (9+6) 6

Each one of the splits should work in theory so the focus then turns to which one of them is easier to attain in practice. The split turns out to be easier to attain using a simple integer division (Figure-3). Finding an easy/natural split and an easy/natural completion (in terms of computational effort) is highly indicative that one is progressing in the right direction. It is a useful exercise for the students to think about whether the other splits could be easily obtained in general.

4296

429

6

Figure 3. Splitting a problem into two instances of the same type: Summing the individual digits of an integer.

Natural recursion: This is a mechanical step requiring no thought at all: sumDigits(429)



sumDigits(n/10)

where the / denotes integer division.

Completion: Now assuming we know the answer to the natural recursion we need to ask what needs to be done to that answer to determine the final answer we are interested in. In the above example if the natural recursion sumDigits(429) were to work right then its answer would be 4+2+9 = 15. It is important to emphasize here that we need not be concerned about how sumDigits(429) will evaluate but rather to what it will evaluate (i.e., 15). Given 15, we need to just add 6 to determine our final answer. Hence the completion is: sumDigits(n/10) + n%10 where % is the modulo operator. Assembling the pieces we get our final solution: int sumDigits (int n) { if (n