Lecture 12: Programming Languages & Top-Down Design Example w/ Karel

Most people have at least heard the names of a few of the more popular programming languages: Pascal, C++, FORTRAN, Java, LISP, Ada, and COBOL (I would list BASIC, but I loathe recognizing it as a legitimate programming language). How many programming languages are there? There is a web page with a list of over 2300 programming languages that has been compiled at the University of Kansas.

What is the difference between all these languages? Why would we need so many? Do we really need more than one? Are there problems that can be solved in some languages, but not in others? Are there problems that are easier to solve in some languages than others?

These are all very important questions! Knowing what language to use, or how to choose an appropriate language is a critical skill for computer programmers. There are really two perspectives from which to answer the questions. On the practical side of the coin, different languages were created with specific kinds of problems in mind. As a result each language will have a unique set of primitive commands or vocabulary that determines the type of problems that are easily addressed in that language. For example, COBOL (COmmon Business Oriented Language) was designed for text and file processing applications for business, FORTRAN (FORmula TRANslator) was created for scientific, numeric computation, Pascal (named after the mathematician Blaise Pascal) was originally meant to be an educational tool, and Java (why is it called Java?) was built to be a portable language for controlling consumer electronic devices. The very nature of the problem often leads to a natural choice of programming language.

From a theoretical perspective, however, we see a very different relationship between most of the programming languages ever created. Alan Turing, as one of his most important contributions to the field of computer science, developed a mathematical model of computation called the Turing Machine. Turing and his Ph.D. advisor Alonzo Church conjectured that the Turing machine was the most powerful model of computation possible (Church-Turing Hypothesis). This conjecture forms the basis of much of the philosophy of computation. So what does this have to do with programming languages? Well it has been shown that a programming language is equivalent to a Turing Machine (called Turing equivalent or Turing universal) in terms of the problems it can solve if it possesses three simple abilities.

  1. Sequence
  2. Selection
  3. Iteration
Sequence is the ability to specify the relative order of the instructions in a program (i.e., what comes first, etc.). Selection is the ability to make decisions, of which there are two varieties: deciding for or against and deciding either/or. For or against is a decision of whether to take some course of action, or none at all. Either/or is a decision between two possible courses of action. Finally, iteration is the ability to repeat an action in a loop.

One consequence of the Church-Turing hypothesis is that any language that supports these three capabilities is equivalent to all of the Turing universal languages. Most of the languages on the language list are Turing universal, including all of those listed above. This means that if a problem can be solved in one Turing universal language it can be solved in all of them!

So to summarize, although there is a practical difference in how the primitives of most programming languages affect the ease with which a problem can be solved, there is no theoretical difference at all in their ability to solve it.


Problem: Karel signed up to run a hurdle race, but doesn't know how to do it. You job is to write a program that will help him. A picture of a sample race is shown here on the left with the route he should take drawn in orange. Karel is to run east along first street, starting at first avenue, until he reaches a corner with a beeper marking the end of the race. Along the way he may encounter hurdles (walls presenting an obstacle across first street). He must jump each hurdle in order to continue moving east towards the finish line. He can not jump several hurdles at once. In other words he must visit each intersection on first street as he travels east. The hurdles can vary in height, with the only limitation being that they are finite. You can assume that Karel starts out facing east.

So how do we solve this problem? It seems overwhelmingly complex, doesn't it? You likely wouldn't even know where to begin. This is exactly what top-down design is for. Let's see if we can decompose the problem in order to make it simpler. Remember there are two rules of thumb for doing this: look for repetitive parts, and look for distinct phases. So is there something that Karel will be doing many times in the course of running the race? Certainly. He will have to solve the problem of advancing one block east on first street. If he know how to do that, then would the problem be simpler? [Notice that this question is already dealing with step three of top-down design: combination.] Consider the following program segment:

while not-next-to-a-beeper do begin
         advance-one-block;
end;
turnoff;
This is a solution to the problem! The above fragment of code can be placed between the BEGINNING-OF-EXECUTION and END-OF-EXECUTION lines of the program. The only (sub)problem now is to figure out how to solve advance-one-block. However, this is a simpler problem than the original. We have made some progress.

Well, to advance one block, one of two situations will arise. Either there is no hurdle in front of Karel, in which case we can just tell him to move, or there is a hurdle that he must jump. Once again we can defer the complexity of jumping and solve this problem as follows:

define-new-instruction advance-one-block as begin
         if front-is-clear then begin
                  move;
         end
         else begin
                  jump;
         end;
end;
There are several things to note about this code. First, look at how Karel's vocabulary can be expanded. The DEFINE-NEW-INSTRUCTION facility allows us to define a new word in terms of words we already know (in most programming languages these are called procedures or functions). Also, we have only been able to solve this subproblem by creating a new subsubproblem, jump, which now needs to be solved. Fortunately, jump is simpler than advance-one-block. Finally, observe the use of semicolons and the words BEGIN and END. Why is there no semicolon after the first end? The rule for semicolons is that they are places after each instruction, except not before an ELSE.

So how do we teach Karel to jump? Here is a case where we can identify three distinct phases: first jump up, then cross over, then fall down. So here is a simple implementation of this decomposition:

define-new-instruction jump as begin
        turnleft;
        jump-up;
        cross-over;
        fall-down;
        turnleft;
end;
Why did we have Karel turnleft both at the beginning and at the end of the jump? This time the decomposition produced three subproblems, but they will each turn out to be simple enough to solve directly. To jump to the top of the wall Karel must repetitively move north and check to see if his right is clear. To fall down he simply moves until he gets to first street (his front will be blocked when he gets there). Here is the rest of the code:
define-new-instruction jump-up as begin
         while right-is-blocked do begin
                  move;
         end;
end;

define-new-instruction cross-over as begin
         turnright;
         move;
         turnright;
end;

define-new-instruction fall-down as begin
         while front-is-clear do begin
                  move;
         end;
end;

define-new-instruction turnright as begin
         turnleft;
         turnleft;
         turnleft;
end;

For a copy of the full program click here.