Computer Science Canada Compare and Contrast #1 |
Author: | wtd [ Fri Feb 18, 2005 11:34 pm ] | ||||||||||||||||||||||||||||||||||||||||||||
Post subject: | Compare and Contrast #1 | ||||||||||||||||||||||||||||||||||||||||||||
The question. What is one of the biggest problems with computer science education today? The answer. In my somewhat educated opinion, the biggest problem is that programming concepts are illustrated with only a single programming language. Why is this a problem? The problem with teaching a single programming language to illustrate programming concepts is that it binds those concepts to specific syntax and a specific way of solving a problem. What is the answer? I'm tempted to say the answer is to teach concepts separately from practical application in an actual programming language. However, this tends to quickly test the patience of students, many of whom, despite having the potential to be good programmers, become disenchanted with not seeing results. It also robs students of an opportunity to see that programming really isn't that hard, and perpetuates the idea that programming is somehow magical or requires a deep level of theoretical knowledge. Of course, deep theoretical knowledge doesn't hurt, but it isn't necessary for a student to get started. I've come to believe that the solution lies in not teaching less programming, but more. Teaching a single language tends to bind specific concepts to specific syntax, but teaching two or more languages could prevent that from happening, especially if those languages espouse fundamentally varied means of solving a problem. Which languages am I thinking of? Ask anyone who knows me. I'm a big fan of Ruby, so Ruby is one of them. It is a language that makes object-oriented programming easy and even fun. It's also a very dynamic language, witha lot of interesting possibilities at run-time. These two characteristics make it a good choice as one of the three languages used here. I recently warped my mind around Haskell. In many ways it's a delightful language. It makes functions values which can be passed around just as one would any other value. It infers the types of values and functions without the need the manually state them in all but a very few cases. Haskell has no looping constructs, forcing programmers to think in a recursive manner. Additionally, Haskell can be interpreted, allowing for easy testing of code without a save -> compile -> run cycle. Last, but not least, Ada95 provides a more verbose, strict, and procedural alternative to the previously mentioned languages. Though it supports object-oriented programming, I'll be limiting my samples to a simpler subset of Ada95. Ada95 rounds out what I believe to be as comprehensive a look at programming in practice with only three languages. What do I expect? I expect anyone reading this knows their way around the command-line on their perating system of choice, whether it's Windows, Linux, or Mac OS X. All examples will be the same regardless of operating system, so I don't think this is too much to expect. I expect that you can recognize patterns. I will not stop to explain syntax in great detail. The purpose of this document is to raise your thinking above concern over mere syntactic details. With having three different languages and examples in all of them, you should be able to notice patterns about how different parts of the language are used. I expect you to ask questions. Don't wait to ask a question. Do so as soon as you encounter something you can't figure out. You'll be far more troublesome if you wait to ask and it turns out your confusion stems from something from earlier in this document. Make sure you understand one section well before moving on. What do you need? A computer with Windows, Linux, or Mac OS X, the Ruby interpreter, Hugs (a Haskell interpreter), GHC (the Glasgow Haskell Compiler), and GNAT, which provides a compiler for Ada95. All Are free, and instructions on how to obtain and install them can be found online. If you need help feel free to ask. A first program. No "Hello world" this time around. I think we should jump in with something more complex and which can demonstrate a number of things. I want to input precisely ten grades, find the average and output it. So, let's look at outputting an initial message. Ruby
Haskell
Ada95
Nothing too complicated here. But let's refactor a bit, and put this into a separate function or procedure. Refactoring is simply taking code, and not extending its capability but changing its form. Ruby
Haskell
Ada95
Why do we refactor code? Compare and contrast the previous two sets of examples. In the end, both achieve the same thing. However, in the first the code is merely telling us how it's doing what it's doing, but not why. Explaining the why is the reason programming languages have the ability to contain comments, and yet, if the code itself can contain that information, isn't that better than a comment? There's another side to this as well. By creating a function or procedure which wraps up that bit of code, we put all the responsibility for how that action is undertaken in one spot. If we use it once or a hundred times in a program, we need only modify the how in one place to affect all of those uses. Getting a grade So, the next step is to create a function or procedure which will get a grade from the user. If the user enters invalid data, the program should assume zero. Ruby
Haskell
Ada95
So, the goal was to read in an integer and, if the input wasn't a valid grade (any integer), assume zero. More than the previous example this shows three distinct approaches to programming. The Ruby example is very simple since calling the "to_i" method on invalid data returned by the "gets" method returns zero anyway. In the Haskell code, we read in the user's input and parse it with the "reads" function. If that is an empty list, then there was no valid data input. We then explicitly test for this, and either return zero or extract the grade from the parsed input and return that. Both of those methods emphasize the use of functions, where "get_grade" and "getGrade" return the grade. In the Ada example we look at a procedural approach. The "Get_Grade" procedure takes an integer argument and modifies it. So, how do we identify bad input and default to zero? In this case, Ada throws an "Ada.IO_Exceptions.Data_Error" exception which we can handle. Handling an exception like this frees us from having to clutter the main portion of the procedure with code to check and see if the input is valid. Getting a valid grade The next bit of progress is modifying the function/procedure to only accept grades from zero to one hundred. Ruby
Haskell
Ada95
Again, three distinct approaches to solving this problem are present. In the Ruby code an exception is used. This particular exception is thrown when we check to see if the grade input falls between zero and one hundred. If the exception is thrown, the "rescue" clause catches it and prints an error message. We then use Ruby's special "retry" capability to go back and try the main section of the method again. As you remember, the default behavior of the "to_i" method is exactly what we want when dealing with no number input at all, so we don't have to explicitly check for that. Again, in the Haskell code we use an explicit check to see if the grade input is between zero and one hundred. As a means of retrying the function, though, we use recursion. This is really much the same approach as that seen in the Ruby example, except that we have no special retry syntax. The Ada example takes a slightly different approach. As in the Ruby example, an exception is utilized (in addition to the exception introduced previously), but in this case it's not the result of an explicit test but rather a feature of the Ada type system. I've created a sub-type of integer which acts exactly like a regular integer (which we used previously), but can only contain numbers ranging from zero to one hundred. The program will vigorously enforce this restriction (or "constraint"). Any attempt to store a larger or smaller number in a variable of this new sub-type will result in a constraint error, which can be caught. Further, this is all wrapped up in an explicit loop, unlike the Ruby or Haskell examples. Under ideal conditions the program will prompt for a grade, accept input, and then exit the loop, running only once. If however, an invalid grade is input, a constraint error is thrown and caught. This skips over the "exit", causing the loop to repeat after an error message is printed. As before, a data error results in a grade of zero being assumed. It's starting to come together Now, we just need to collect and average the ten grades. Ruby
Haskell
Ada95
The Ruby example proceeds in a fairly straightforward fashion. In addition to the code previously written to get a single grade we add a function which gets ten grades. It does this by means of a simple loop which runs ten times, and each time gets a grade and adds it to the end of an array. That array then gets returned to the rest of the program. Taking this approach, we get all of the grades, and then later we can take whatever information we want from those grades. The Haskell example takes a very similar approach, but uses recursion rather than a simple loop. Here we also use overloading: two functions with the same name, but different arguments. When the argument for the number of times to loop is zero, it returns an empty list and terminates the recursion. Otherwise it gets a single grade and attaches that to the list formed by getting the rest of the grades. Again, we have ten grades we can work with later. The Ada example takes a different approach entirely. It's a rather more pragmatic approach. The problem doesn't ask for anything other than the average grade, so all we have to do is keep a running total and then divide that by the number of grades, and that's all the Ada procedure does. A little more Ruby and Haskell work So, the Ada example is pretty much done, except for outputting the average. The Ruby and Haskell examples still need some work to sum and then divide that sum by the number of grades. Ruby
Haskell
The Ruby example here takes a rather conservative approach to finding the sum. We start out with a sum of zero, then add each grade to it. In the end, the sum gets returned. The means of finding the average is similarly mundane. Simply divide the sum by the number of grades. Involved in this is a conversion of the sum to a floating point number, so the output is a floating point number. The Haskell example doesn't involve any explicit looping or recursion. Instead we use the "foldr" function. This function works by taking a function which in turn takes two arguments and applying it to each element of a list. The other argument to this function represents the result of the previous evaluation. The second argument to foldr is the initial value. If we had the grades 80, 70, and 90, for instance, running the function would look like the following.
Since we never provide the final argument to foldr, which would be the list of grades, we simply get a function we can later apply to any list of grades. For convenience we give it the name "sumGrades." As in the Ruby example, the Haskell example for getting the average is pretty straightforward. The grand finale At last, we put it all together and output the average grade. Ruby
Haskell
Ada95
That's not all Of course, it's important to note that in many cases the technique used in one language example could be easily applied to another language. It would be relatively simple to have the Ada program collect all input grades into an array, for instance, much as the Ruby program did. Along the same lines, Ruby can approximate Haskell's foldr.
Give it time to digest Once you've read to this point go back and read through again. Compare and contrast the samples provided. They perform the same task, but they go about it in different ways, and that's important. There is more than one way to do things, and often those approaches transcend the syntax of the language and can be applied to programming in general. |