[Bluej-discuss] Programming as random process [LONG]
Stephen Bloch
sbloch at adelphi.edu
Thu Apr 20 16:15:07 BST 2006
Gordon Royle writes:
>One thing that increasingly frustrates me about trying to teach Java
>is the number of students who seem to view programming as some sort
>of random process where they type anything at all and then come and
>complain that "it doesn't work".
Amen, brother!
>I use BlueJ precisely for the purpose of removing ALL of the things
>like "public static void main(String[] args)" where you just have to
>say "type this in without understanding it and we'll explain it
>later", so in general they should be able to understand every part of
>the classes that they are assigned to write..
Amen, again.
>How can I overcome this?
If anybody finds a "magic bullet" for that, we'll all be happy.
However, here are some thoughts.
Aryeh Friedman writes:
>is always true to some extent. One way to conteract (and no offense ment to
>the text) is start with very simple programs like hello, world from the
>*COMMAND LINE* not a IDE then introduce the IDE for more advanced stuff.
I disagree with Aryeh, and agree with Gordon, on this: it requires
introducing a lot of concepts as "black magic -- trust me, I'll
explain later". One of BlueJ's greatest strengths is that it allows
you to postpone discussion of "public static void main (String[]
args)" until you're ready, and it allows you to postpone discussion
of I/O until you're ready. You can introduce each of these concepts
ONE AT A TIME, each with its own motivation, rather than throwing
them all at the students on the first day of CS1.
To my mind, the other of BlueJ's greatest strengths is the automatic
class diagrams. Which isn't particularly useful until you've got
several classes interacting, i.e. not the first few weeks of CS1.
One problem I've found with starting in BlueJ is that it's so easy
for students to invoke methods and constructors with the mouse that
they never learn to do it with Java syntax. To avoid this, I start
in DrJava or ProfessorJ (which, like BlueJ, allow you to invoke
methods and see the results directly and therefore postpone
discussing "public static void main" and I/O until you're ready), and
switch to BlueJ later when the programs get complex enough to need
class diagrams, and when students are fluent enough with Java syntax
that they won't use the mouse as a crutch.
Another advantage of DrJava and ProfessorJ over BlueJ, for absolute
beginners, is that they enforce language subsets. If a first-month
student misplaces some curly braces, how do you explain the resulting
error message that talks about an "inner class"? Better to just make
inner classes, and various other things that the first-month student
doesn't need, illegal and produce a level-appropriate error message.
Here's another issue. BlueJ (and DrJava and ProfessorJ) makes it
easy to experiment and play with objects. This is mostly a good
thing: I WANT students to get a hands-on feel for how objects behave
by trying things and looking at the results. But it also, perhaps,
encourages the sort of "modify the program randomly and see if it
works" philosophy Gordon complains of. So it needs to be combined
with a rigorous, step-by-step "design recipe" (to use the terminology
of Felleisen et al's _How to Design Programs_): when faced with an
assignment in English, this is what you do first, and this is what
you do next, until after, say, 6 steps you have working, tested code.
I tell my students that I WILL NOT HELP THEM with step N until they
show me a satisfactory solution to step N-1. (If I have TA's or
tutors, I tell them to be equally strict.) My grading rubric
reinforces this step-by-step design recipe: you get a certain number
of points for doing step 1 correctly, and a certain number of points
for step 2, etc. Even if you don't end up with working, tested code,
you get partial credit for having (say) correctly specified the
interface and written a good test suite.
So what is this step-by-step recipe? Well, there are different
recipes for constructing a method, constructing a class, constructing
a collection of related classes, etc. but here goes
To Design a Method
1) Identify the data types it takes in and the data type (if any) it returns
2) Write a method header, specifying return type, method name,
parameter names and types
(with a "purpose statement" if it's not obvious)
3) Write a test suite, i.e. a collection of several examples of
invoking the method, each with a specified "correct answer"
4) Write a "method skeleton", in which you list (in comments) the
expressions available to you and their types
5) Choose among these expressions the ones relevant to the problem at
hand, and write code that computes the right answer from those
expressions
6) Run your test suite, identify and fix any bugs found
For example, here's a recent assignment I gave:
1) Define a class HotelRoom with five fields: the room number, how
many king-sized beds it has, how many twin beds it has, how much it
costs per night (in cents), and whether it's non-smoking. Be sure to
include a constructor that allows people to specify the values of all
five fields. Also write at least two examples of instances of the
class, showing how to extract the values of various fields from them.
2) Add a method named capacity to the HotelRoom class that computes
how many people can sleep in the room. Assume that two people fit in
a king, one in a twin, and nobody's on the floor or in the bathtub.
3) Add a method named ok which takes in a number of people and a
boolean indicating whether they want smoking or non-smoking, and
tells whether the specified room will suit their needs (having at
least enough sleeping capacity and the right smoking flag).
[problems 4-14 omitted]
Let's assume the student has already done problem 1 and produced
class HotelRoom {
int roomNumber;
int kings;
int twins;
int price; // per night, in cents
boolean nonSmoking;
HotelRoom(int roomNumber, int kings int twins, int price,
boolean nonSmoking) {
this.roomNumber = roomNumber;
this.kings = kings;
this.twins = twins;
this.price = price;
this.nonSmoking = nonSmoking;
}
}
class HotelRoomExamples {
HotelRoom room1 = new HotelRoom (12, 1, 0, 5995, true);
HotelRoom room2 = new HotelRoom (15, 1, 1, 7995, false);
HotelRoom room3 = new HotelRoom (121, 0, 4, 8500, true);
HotelRoomExamples () {}
boolean testBasics () {
return (this.room1.roomNumber == 12) &&
(this.room1.kings == 1) &&
(this.room1.twins == 0) &&
(this.room1.price == 5995) &&
(this.room1.nonSmoking == true);
}
}
(The "testBasics() method isn't really testing anything that could
possibly go wrong, but it gives students practice constructing
instances of the class and extracting their fields. I haven't
introduced "private" yet at this point in the semester.)
Now the student starts on problem 2. For step 1, I would expect to
see (somewhere in the HotelRoom class)
// The "capacity" method takes in no additional information, and
returns an int.
For step 2, the student adds the lines
int capacity () {
...
}
which are essentially step 1 translated into Java syntax.
For step 3, the student adds to HotelRoomExamples the lines
boolean testCapacity () {
return (this.room1.capacity() == 2) &&
(this.room2.capacity() == 3) &&
(this.room3.capacity() == 4);
}
boolean testEverything () {
return this.testBasics() &&
this.testCapacity();
}
(In this case, no additional example rooms were necessary, but the
student should consciously choose test cases at this step to make
sure all cases of the code are tested.) The process of choosing test
cases helps students to clarify in their minds what the method is
really supposed to do, and particularly illuminates borderline cases.
For step 4, the student adds a "skeleton" to the "capacity" method, getting
int capacity () {
/*
this HotelRoom
this.roomNumber int
this.kings int
this.twins int
this.price int
this.nonSmoking boolean
*/
...
}
I liken this step to getting all the ingredients out of the cupboard
and arranging them on the counter before I start baking a cake.
For step 5, the student observes that roomNumber, price, and nonSmoking
are irrelevant to the problem at hand (but let's leave them in the comment,
so we can copy-and-paste that list into each subsequent method we
write) and adds the line
return 2 * this.kings + this.twins;
For step 6, the student runs new HotelRoomExamples().testEverything()
. If it returns true, the student pops the champagne and celebrates
the completion of Problem 2.
Then the student goes on to Problem 3. I'll leave the various steps
to you, pointing out only that in step 4 the "method skeleton" has
some additional lines:
boolean ok (int people, boolean wantSmoking) {
/*
this HotelRoom
this.roomNumber int
this.kings int
this.twins int
this.price int
this.nonSmoking boolean
this.capacity() int
people int
wantSmoking boolean
*/
...
}
Naturally, as classes get bigger and more complex, the "method
skeletons" get unwieldy, so after a certain point in the course,
students are allowed to "pre-select" the expressions likely to be
relevant to the problem at hand.
By the way, these "method skeletons" are especially helpful with
recursive data structures: they give you a correct recursive function
definition before you have time to notice that recursion is
difficult. Consider
interface HotelRoomList {
int countRooms ();
boolean anyOK (int people, boolean wantSmoking);
int countOK (int people, boolean wantSmoking);
HotelRoomList extractOK (int people, boolean wantSmoking);
// etc.
}
class EmptyRoomList implements HotelRoomList {
int countRooms () {
return 0;
}
boolean anyOK (int people, boolean wantSmoking) {
return false;
}
int countOK (int people, boolean wantSmoking) {
return 0;
}
HotelRoomList extractOK (int people, boolean wantSmoking) {
return this;
}
}
class NonEmptyRoomList implements HotelRoomList {
HotelRoom first;
HotelRoomList rest;
NonEmptyRoomList (HotelRoom first, HotelRoomList rest) {
this.first = first;
this.rest = rest;
}
int countRooms () {
/*
this NonEmptyRoomList
this.first HotelRoom
this.rest HotelRoomList
this.rest.countRooms() int
*/
return 1+ this.rest.countRooms();
}
// ...
HotelRoomList extractOK (int people, boolean wantSmoking) {
/*
this NonEmptyRoomList
this.first HotelRoom
this.rest HotelRoomList
this.first.ok() boolean
this.rest.extractOK (people, wantSmoking) HotelRoomList
*/
if (this.first.ok()) {
return new NonEmptyRoomList (this.first,
this.rest.extractOK(people, wantSmoking));
}
else {
return this.rest.extractOK(people, wantSmoking);
}
}
}
Now for one more thought. I don't start students in Java if I can
possibly help it: I much prefer to start in Scheme, where the syntax
and semantics are much simpler and more consistent than in Java, and
where the widely-used and well-tested DrScheme IDE enforces language
subsets and provides support with parenthesis-matching, indentation,
etc. Once students have learned in that syntactically-simpler
setting the important concepts
* variable
* function
* parameters
* function composition
* data types
* conditionals
* types with fields, aka "definition by parts"
* types with variants, aka "definition by choices"
* self-referential data types and functions (once you already know
how to work with definition by parts and definition by choices
individually, just combine them in a certain way)
* higher-order functions (either taking in or returning functions as
first-class values), including GUI callbacks
* maybe (if I have time) I/O and assignment statements
we make the transition to Java and show how all the same concepts
arise with different syntax.
I've taught Scheme-first a number of times, both to CS majors and to
non-majors, I've taught Java-second to students who had started in
either Scheme or C++, and I've taught Java-first a few times. This
term is the first time I've taught the Scheme-to-Java transition all
within CS1, and I think it's working quite well.
--
Stephen Bloch
sbloch at adelphi.edu
More information about the bluej-discuss
mailing list