This paper describes a proven and language-independent technique for teaching computer programming. The technique employs typical-use examples and minimal examples to illustrate language features and programming concepts. Both types of examples are presented to students in the class-room and are made available on-line.A typical-use example is a simplified version of a real application. It may be short, but is rarely short enough to be presented in its entirety. A minimal example is focused on a single feature or programming concept. It is a complete executable program, crafted to be as short as possible. Minimal examples rarely compute useful results, because so much is left out. However, minimal examples can illustrate key language features with impressive brevity and clarity, because everything not directly relevant is left out.
Keywords: Programming, C++, Perl, Make
We present a technique for teaching programming by example. Our approach is novel in so far as we make heavy use of minimal examples as well as the more traditional typical-use examples.
A typical-use example is a simplified version of a real application. It is short, but usually not short enough to be presented in its entirety in the class-room. A minimal example is focused on a single feature or programming concept. It is a complete executable program, crafted to be as short as possible. It is instrumented for observability with no attempt to do anything other than to illustrate a point.
Minimal examples advantage both student and teacher. For the student, they facilitate self-study. Students can quickly read, modify, and observe programs and program behaviour. Minimal examples can also be used by students to focus their questions to the instructor and for problem isolation during debugging.
For the instructor, minimal examples are ideal for class presentation as they often fit on a single transparency. Once the examples have been prepared, they can be easily modified for use in exams. A question based on a minimal example usually takes the form: given a program, provide the exact output. These questions are revealing since there is no way to determine the correct output without a thorough understanding of the source code. The answers are easy to grade as they are short and there is only one correct answer.
Our first goal in presenting this work is to demonstrate the use of minimal examples in teaching programming. Our second goal is to promote a certain kind of paper for contributions to WCCCE in the future. Often new teaching proposals, even very good ones, require a substantial change in our current approach. Most of us attending WCCCE are so busy that we cannot find time for such change, and so we often neglect promising teaching proposals because we lack the time to implement them. However, many instructors know of simple, "standalone" techniques that can be thoroughly explained in 20 minutes and can be put into practice with only a small change in our current approach. Like the "little language" approach popularized by languages like Awk, these "little papers" can offer surprisingly good results for modest effort. We hope that this little paper will be followed by others like it at future WCCCE meetings.
We have applied minimal examples with excellent results to learning and teaching C++. Our standard approach to a tricky C++ feature is to develop a small set of minimal examples that answer the most important questions about the feature. Usually, the shorter the example, the quicker we can learn from it and the more confidence we have in the answers it yields. As instructors, developing the examples nearly always exposes misunderstandings we have about the feature.
In the remainder of this section, we present minimal examples illustrating (1) constructors and destructors, (2) exceptions, and (3) assignment versus initialization. We have written dozens of other minimal examples illustrating templates, inheritance, virtual functions, and other C++ features.
Throughout the paper, each minimal example is presented in the same format:
Driving question:
the question that the minimal example is designed to answer.
Often an example is developed in response to some confusion about
a language feature.
Initially, the question is usually vague and ill formed;
the question and the "minimal answer" are developed iteratively.
Program:
the complete source code of the example.
| Output:
the exact program output.
| Discussion:
a brief discussion of the main points illustrated.
| |
#include <iostream.h> class C { public: C(int i0) : i(i0) { cout << "C: " << i << endl; } ~C() { cout << "~C: " << i << endl; } private: int i; }; C c1(1); // static int main() { cout << "main start" << endl; C c2(2); // automatic cout << "main middle" << endl; C* C3 = new C(3); // dynamic cout << "main end" << endl; }
C: 1 main start C: 2 main middle C: 3 main end ~C: 2 ~C: 1
#include <iostream.h> class X {}; void f() { cout << "before throw" << endl; throw X(); cout << "after throw" << endl; } int main() { cout << "main start" << endl; try { cout << "before call to f" << endl; f(); cout << "after call to f" << endl; } catch (X) { cout << "X caught" << endl; } cout << "main end" << endl; }
main start before call to f before throw X caught main end
#include <iostream.h> class C { public: C(int i0) : i(i0) { cout << "C(int) i: " << i << endl; } C(C& c0) : i(c0.i) { cout << "C(C&) i: " << i << endl; } void operator=(C& rhs) { cout << "operator=(C&) lhs: " << i << " rhs: " << rhs.i << endl; } private: int i; }; int main() { C c0(0); // initialization C c1 = 1 // initialization C c2 = c1 // initialization c0 = c1; // assignment }
C(int) i: 0 C(int) i: 1 C(C&) i: 1 operator=(C&) lhs: 0 rhs: 1
It cannot be overemphasized that assignment and initialization are different operations. [Stroustrup93,page 239]However, because the "=" symbol may appear in both assignment and initialization, even experienced C++ programmers sometimes confuse the two. This example is based on the class C, instrumented for observability. Here it is the constructors and the operator= function that are instrumented. (With its void return type, this operator= is unconventional but is sufficient for our purposes.) In main are four simple statements showing that the distinguishing characteristic of initialization and assignment is not the "=" symbol. Rather, the crucial issue is whether the object is being created for the first time (initialization), or already existed and is being overwritten (assignment).
Perl [Wall91] is a public-domain language developed by Larry Wall, and has been ported to the Unix, DOS, and Macintosh platforms. Perl is a blend of the Unix shell languages, and the Unix awk, sed, and grep utilities. It is widely used by programmers, systems administrators and database administrators.
An array in Perl is a sequence of scalars. How are arrays of composites (multi-dimensional arrays) emulated?
$;=A; $x{1,1}=a; $x{1,2}=b; $x{2,1}=c; foreach $key (keys(%x)) { print "x{$key} = ",$x{$key},"\n"; }
x{1A1} = a x{2A1} = c x{1A2} = b
Perl emulates multi-dimensional arrays using associative arrays. An associative array in Perl consists of zero or more key/value pairs. Each key and each value must be a scalar: a string or a number. A multi-dimensional reference is joined into a string that is then used in the normal way to index an associative array. For example, x{1,1} is translated by Perl into x{join($;,1,1)}. The effect of this is to join the strings "1" and "1" together into a single string. The join function uses the value of the special variable $; as a separator. Above, we assigned $; the value "A" (the value of $; defaults to null). Consequently, the join-produced string is "1A1" and the the value of x{1,1} is the scalar associated with the string "1A1".
What strings are matched by the Perl metacharacters +, *, ^ and []?
@a = ('bb', 'bb*', ' *', ' +', '^b', '[^b]*', ); $s0 = "aa*bbb+1234?"; foreach $s (@a) { if ($s0 =~ /$s/) { print "$s: $`!$&!$'\n"; } else { print "no match\n"; } }
bb: aa*!bb!b+1234? bb*: aa*!bbb!+1234? *: !!aa*bbb+1234? no match no match [^b]*: !aa*!bbb+1234?
The string $s0 is matched against all the patterns in @a. For each pattern, the pattern and the value of $s0 are written out on a single line . In the output, $s0 is partitioned such that the substring matched by the pattern is enclosed by a pair of exclamation marks ('!'). Note the distinction between "no match" and a match on the empty string.
The meanings of the six patterns in @a are as follows:
'bb':
matched by a string with two consecutive 'b's.
'bb*':
matched by a string with a single 'b' followed by
zero or more consecutive occurrences of b.
| ' *':
matched by a string with zero or more consecutive blanks.
| ' +':
matched by a string with one or more consecutive blanks.
| '^b':
matched by a string with 'b' as the first character in the
string.
| '[^b]*':
matched by a string with zero or more consecutive occurrences of any
character other than a 'b'.
| |
Make is a Unix tool that streamlines the process of generating and maintaining
object files and executable programs.
Make allows the programmer to compile programs consistently
and eliminates unnecessary recompilation of
modules that are unaffected by source code changes.
Make reads a user-created program that contains information on
what files to build and how to build them.
Make programs take the form:
target...: [dependency ... ] [rule] ...
In what order are targets processed by make?
a: b c touch a c: touch c b: touch b
touch b touch c touch a
Once make begins, it processes targets as it encounters them in a depth-first dependency scan. In the above example, let us assume that the files a, b and c do not exist in the current directory. Make starts with target a. Target a has dependencies b and c. They have not been checked yet. Consequently, make defers processing a until b and c have been checked against any dependencies they might have. Next, b is checked. It has no dependencies so make considers it out of date and builds it by executing its associated rule. Next, c is checked. It has no dependencies so make considers it out of date and builds it by executing its associated rule. Finally, the way is clear to process a. Since at least one of a's dependencies have been modified, a is considered out of date and must be built by executing its associated rule.
We have presented minimal examples in C++, Perl, and Make, showing how just a dozen or so carefully chosen lines can yield impressive clarity. With minimal examples, the guiding principle is "more is less". We trim the examples brutally, asking the same question about every keystroke: "Can it be omitted?".
We routinely "instrument" the examples, using print statements to make selected aspects of program behavior visible. Frequently, an example is little more than a few uses of a language feature, plus the appropriate instrumentation.
While this paper emphasizes minimal examples, it is important to note that they are most effective in combination with typical-use examples. In this paper, we have emphasized minimal examples because most readers are already familiar with typical-use examples.
We have used minimal examples successfully in first, second, third, and fourth year courses, and in industrial short courses. Across programming languages and skill levels, we have found minimal examples to be an effective technique: enjoyable to create and present, and revealing and easy to grade on exams.