Saturday, April 26, 2008

Done with teaching!

I spent the last three and a half months teaching a programming languages course and working on my dissertation, which is why I haven't posted much here of late. The worst part about the teaching job was that it was in a different city than my home and had an awkward schedule, so I had to commute and stay at someone else's house for the majority of the week. It also meant I couldn't make any of the Toronto Lisp Meetings.

I used Shriram Krishnamurthi's text Programming Languages: Application and Interpretation for the course, which seemed to go over well, despite difficulties. The students coming into the course are mostly exposed only to Java and C/C++, so Scheme tends to break a few brains at the beginning. I'd like to say the whole class had a firm grasp on Scheme when it was over, but I can't.

For the most part, students could write Scheme code, but it was mostly by translating (not very good) Java to Scheme. I'm not sure why. For example, there was an awful lot of this in assignment submissions:

(if (predicate? foo)
    (begin
      (set! ret-val (compute-something ...))
      ret-val)
    ...)

To say that I stressed the idea of expressions evaluating to a value and avoiding mutation in my lectures, labs and through assignment feedback would be an understatement. I'm not sure why this mindset persisted, but I hypothesize it has something to do with wanting to make the simple complex. Or maybe it has something to do with not seeing the forest for the trees. I really don't know.

I had a difficult time getting across the idea of trusting an abstraction, even when comparing to something like the APIs used frequenty in the Java libraries. For example, many students just couldn't deal with continuations without knowing what happened inside them. One even remarked he wished it had a toString method. It took a lot of convincing that seeing #<continuation> in DrScheme was basically the same thing.

Student: "But I don't know what's happening inside it!"

Me: "That's the point: it doesn't really matter."

The same symptom could be seen in trying to understand functions as values, except that I flogged the notion of closures so much that some students' brains couldn't help but give up and understand it. :)

Another thing that struck me was how, after explaining macros in Scheme, most tried to write macros as you would in Common Lisp. I expect its because CL macros let you manipulate the code directly, which fits with the theme of getting at the guts of something. Maybe there's something deeper at work here, but I'm not going to go wild in my conjectures based on this small sample.

After this class, I'm more convinced that starting with something like Scheme followed by Java would be an easier transition than the other way around. Whether starting with Scheme is easier than Java is another question entirely.

Oddly enough, they seemed to grasp Prolog pretty well...

17 comments:

grant rettke said...

If you start with Java, it doesn't allow you to "deal in concepts", instead it allows only the horribly verbose, and literal.

johnm said...

Starting with Scheme and then moving on to languages like Java is definitely easier/better.

I find it disheartening that places like Berkeley and MIT have moved away from teaching Scheme in the introductory course.

steve said...

I'm not a student, and I agree with your students. There is a good reason why Lisp (and Scheme) programmers know the value of everything and the cost of nothing... because they trust the abstraction rather than understanding what is happening inside.

SICP is written arse-backwards, building algorithms and approaches with absolutely nothing underneath until the last chapter, with only some "this is slow, so we do it like this" throughout the book. WHY is it slow? That's where needing to understand what is happening inside of the abstraction becomes important.

It really says something that your students can cope with Prolog much better, so I would be examining the approach you're using rather than commenting on your students as an abstract body. You need to understand what's happening, rather than abstracting over your student class!

Anonymous said...

steve, if you were a student you would have been taught how to analyze and design algorithms for asymptotic runtime (often called Big-O Notation) and that this is what matters most for speed. Then, when programming lisp or whatever language you like, you'd spend most of your optimizing efforts on this, before profiling and hand-tuning the really critical parts.

"what's happening underneath" applies not only to your computer but also to your brain. many functional languages (read: haskell) are at least decent for runtime and more suited for the untainted or willing-to-change brains. and you can understand pretty well what's going on underneath as well.

in my first semester at university we were taught haskell, which proved to be a really good choice. in haskell you just can't write the kind of code you pasted so you have to look for alternatives and you are forced to deal with the language instead of translating java code. i think this was the case with prolog as well since it is so radically different from anything the kids know.

unfortunately, not telling the students about begin or progn won't solve the problem :)
first haskell, then lisp/scheme?

eokyere said...

Student: "But I don't know what's happening inside it!"

Me: "That's the point: it doesn't really matter."

bullshit.

DanM said...

After working in this field for almost three decades, the one thing I often find most frustratingly lacking in co-workers is the ability to think at varying levels of abstraction. This should be taught explicitly as a skill.

That's not the same as saying you shouldn't understand or inquire into what's beneath an abstraction. Only that you should do so when it's appropriate. Understanding the abstractions that computers work with, from transistors all the way up to high level programming or domain-specific language concepts, and understanding how one abstraction supports and can be used to implement another abstraction, is incredibly informative, and even necessary when flaws are encountered in abstractions.

But at any given time, you should be able to focus your attention on one level within the tree of abstractions, excluding the others, in order to focus your thinking and reduce complexity. Failure to do so wastes time in your own head and in communications with others, and leads to complex code.

I think that many practitioners, especially the young, have only a vague intuition of this layering of abstractions. They know that there are "high-level" (Java, Haskell) and "low-level" (assembly, C) programming languages, but they don't see the many other layers of abstraction inherent in computing. And yet all of science, not just computer science, uses this way of looking at things to manage complexity.

I applaud your effort to teach students to focus on a particular layer of abstraction. Keep trying to get this point across; if they explicitly understand it and learn to move among abstractions at will, they will be far more productive.

Geoff Wozniak said...

@steve: What I haven't told you is that courses on assembly language, C and rudimentary are done before the class I taught, so they are supposed to be familiar with the underlying machine. And I did tell them effectively what happens underneath. Many still insisted that they needed to know the nitty-gritty details.

@eokyere: See danm's comments for why I think you're misunderstanding the point.

@danm: Well said.

pierre said...

(set! ret-val (compute-something ...))
ret-val)

-- that's why you should consider teaching them haskell...

Anonymous said...

I'm am not one of your students, but I did help many of them in the labs. When I took the course two years ago I finished with a high 80 and the took mark on the final.

Saying that learning Scheme and then learning Java is about as true as saying learning to walk after having learned to run is easier than the other way around. Imperative code is much more natural to the way people think. When I tutored high school students I use the analogy of a computer program like a set of instructions on the back of a box of Jello. That is a real world program that they can understand. I can't think of an instance in the real world where I do two things which evaluate to the thing that I want. That is a powerful mathematical abstraction, but it is very alien to our natural way of thinking.

I personally like Scheme but writing in it well requires level of abstraction that are beyond what many people have and so possibly beyond what quite a few are capable of.

Starting with imperative programming lightens the cognitive load and allows instructors to jump (more) directly with introducing students to algorithims. Seriously, if you think teaching Scheme before an imperative language like Java I suggest you question your own pedagogy.

Geoff Wozniak said...

@Anonymous who is not my student by helped them in labs: I think you misunderstood. I said that going from Scheme to Java is an easier transition than the other way. I'm not sure starting with Scheme would be easier, but it would probably make it easier for students to see and appreciate other approaches.

Mark Miller said...

Not really sure why your students were preferring a Java style of Scheme over what you were teaching, but I'll give it a go based on my own experience.

I found C easier to deal with when I was in college many years ago, in the sense of seeing expressions as "first-class citizens". For example saying something like:

value = (sometest) ? value1 : value2

rather than doing an if-then-else for this was fine with me, as was:

return (someValue < 100)

For a time I loved the fact that you could do stuff like:

FILE *hFile = NULL;
for (hFile = fopen("somefile", "r"); !feof(hFile); fgets(buffer, 10, hFile));
fclose(hFile);

I don't know if Java supports this sort of thing.

I think it takes a mathematical sensibility to get what you're talking about. Math educators in algebra stress simplification, and abstraction. I came to like conciseness in my code, I think because of this. In my example here you can see and use the for statement as a function (albeit with no return value) that takes other functions as arguments and executes a looping action with them (for (initializer, tester, incrementer)). The for statement is really a form for looping.

I agree that working at a level of abstraction, just accepting it, and finding it useful is a skill, but the best way to learn it might be from experience.

I can remember also having to learn this. In my high school pre-Calc class I remember my teacher asking, "What is a function?" I thought for a moment, raised my hand, and said something like, "A function acts as a catalyst on input, producing an output." He chuckled and said, "That sounds like chemistry. That's not what I'm looking for." He called on another student who answered, "For every X there is exactly one Y." The reason I answered the way I did is I had been programming for several years by then. I understood the programming of functions, and they worked basically the way I described. But algebra has a specific, abstract definition of what a function is. The other student was right.

I think it would help your students to see Scheme as "a kind of algebra". I mean this in the sense that in algebra the idea is you're working with the abstractions themselves, and you're not worrying about "how does this work?" (even though I did that) The idea is to learn some higher level concepts that are valuable. I think if a student has had that experience of realizing, "If I just work with the abstraction I can understand other things so much better," then they'll understand.

I wonder if (it were possible) bringing in a Lisp machine would help students get it. :)

A big problem you're facing is there are basically two schools of thought on programming, as I understand it: Turing and Church (as in, Alonzo Church). The "Turing" school believes that programming like Fortran, Cobol, Algol, Pascal, C, C++, Java, etc. The "Church" school sees programming as Lisp, Smalltalk, Prolog, Scheme, SML, Haskell, OCaml, etc. The "Turing" school believes that programming is procedural. You give commands to the machine, and it executes them. The "Church" school sees programming as a kind of math. Programming is expressing your ideas in a formal way, and it's up to the machine to interpret and execute them. So I think for the purposes of teaching Scheme, stressing the mathematical approach is important. Encourage the students to get away from procedural, imperative thinking.

DanM said...

To anonymous that helped these students: I'm skeptical of the claim that imperative is more natural. Logical reasoning at the level required for programming is not natural in any meaningful sense, no more so than mathematics beyond the arithmetic of very small numbers is. Students in a computer science curriculum will already have facility with algebra, trigonometry, and more. In those disciplines, functional thinking is the norm, not imperative. Learning functional programming first should pose no special difficulties. These students were probably relating the contents of this course to their experience with Java, rather than with mathematics. Had they learned the languages in reverse order, I doubt that this would have caused any particular difficulty. MIT did this for decades, as I understand it.

Mark Miller said...

To anonymous who did not take class, but helped students with labs:

I've heard this same explanation from others, that most people think in terms of imperative, procedural systems. I think it has more to do with the way they are educated, and indeed the way most people are acculturated in our society. Look at the school systems, look at the classrooms. It is usually based on doing what you are told, following instructions. Students come to understand this model of learning. Programming can be seen as rather like teaching a machine to do something. So naturally the student thinks "Tell the machine what to do."

Languages like Scheme take a modeling approach to programming. You don't tell the computer what to do. You build your model using math-like expressions. The VM then interprets the expressions and puts the model "in motion" for you. As analogies go this probably isn't great, but it's kind of like showing the computer a specification for a blueprint to a building. You don't express the blueprint as a set of instructions. Instead it's a representation model of a real thing (or what will be real once it's built), and what's important are the relationships between the parts. It's a mathematical/engineering model. Programming in a language like Scheme is kind of like giving the computer the blueprint. It interprets the specification, "builds" the house (virtually), and does things like allowing you to tour the house, see what it can do in various rooms, how it looks from inside and out, etc. So you tell it enough information so that it can inference the rest, given some knowledge about "how houses are built from specifications", and it takes it from there.

So perhaps what's necessary to "get" Scheme is for the students to already have some ideas about how to think like engineers, mathematically. The problem is most people don't like math, and I think a large part of the problem there is most math teachers in the public schools, from what I hear, don't like math either. Most these days have little if any training in it. So they're not that competent in the subject, and they don't like it. Not a good setup for trying to teach such languages to students.

Vladimir Sedach said...

Whether imperative or functional programming is "more natural" is debatable, but I think by now experience has overwhelmingly shown that the functional approach to programming produces more robust, maintainable systems in a shorter amount of code (the latter being one of the major reasons for the former pair).

Just because something is easy to do does not mean it is worth doing, and on the flip side anything that is worth doing is worth doing well.

The above principles apply to learning how to program as well. Predictably, I think teaching people imperative programming because "it is more natural" is a dumb excuse for mediocrity.

Anonymous said...

I am the student who helped in the labs: as for which is more natural I still maintain that imperative is more natural. For this I return to my Jello box: we are given procedural instructions everyday at thus people are more comfortable with starting off with them. Everyone has seen and understands cook books. Natural language -it seems to me- to be only amenable to if-then procedural programming.

Perhaps in some ontological way neither is more natural, and that the inclination towards imperative is biased by our culture... but that question has no practical application. The first level of programming I ever learned was batch files in MS-DOS: programming as automating a routine is so natural that every macro recording system uses it. While true great programming requires higher level of abstractions, there is no need to throw students into the deep end right away. After all, many of them will never need it. Actually I wonder how many people that work everyday with a programming language never move beyond that level of understanding. I'm thinking of engineering students crud-app developers in the real world.

I feel throwing students into the deep of pool with higher languages suffer the same fault as those who propose starting students off with assembler: both have a much-too-steep initial learning curve when all you want is students to get used to programming and compiling and thinking in terms of breaking down problems into smaller and smaller pieces without getting lost in the minutae of computing.

PS Oh, math is really not natural for CS students by the time they are learning Java. Frankly, I learned it in high school and most learn it in first year. Many students hate math. Frankly, I still do, but have grown to appreciate since mid-way through 3rd year.

Anonymous said...

I'm more convinced that starting with something like Scheme followed by Java would be an easier transition than the other way around. Whether starting with Scheme is easier than Java is another question entirely.

My undergraduate education at UC Irvine started with a course in Scheme, and then the subsequent two courses (on data structures/etc) were in Java. Ours was a pilot program, where the normal first-year courses were in C++.

My impression was that Scheme was not at all difficult to start with (for me and my classmates). It allowedus to progress past things like "the syntax of conditional statements and loops" much faster than concurrently-running courses in C++, and allowed us to move into more advanced concepts (trees, data structures) sooner.

My experiences may be colored with having taken a high school course in Pascal, and poked a bit at C (but never felt I understood it, at the time) before going to the university. However, I still believe that learning Scheme (we used The Little Schemer text, I believe) would have worked out Just Fine as an absolutely-first-course in programming. I feel that starting with a high-level (yet powerful) language like Scheme or Lisp can be very beneficial, as it helps new programmers not get lost in details which are lower-level than what is needed to solve the problems at hand.

rhyre said...

I think Lisp and Scheme are the best places to start, because the simplified syntax frees your mind to get the concepts.


My 'language order' was BASIC, Assembler, Pascal, Forth [high school], LOGO, Lisp, CLU [college], then C and C++. It's fun to see Java and other languages lurching toward more and more Lisp features
over time.


I too am dismayed to see a move away from Lisp as the first language in MIT's CS program.