20070509

Ruby - Sparkles and Impurities

This is actually an old post that doesn't display properly on my old blog, but which I feel has relevant information for this one. It's also a precursor to a post I'll hopefully make this week or next about the progress I've been making in the last--oh, MONTH? I decided to repost this because I read another good blog post griping about Ruby.

In case you haven't read about it, I've been starting to code in Ruby. It's a fine language in its own right, and I'm having a good deal of fun using it. For ages I've been a serious Perl junkie. I've used it extensively at my last two jobs, and I totally dig it, because I love punctuation. It was sometime during last year that Caius and npj7y bugged me to learn it, so now I am. I read the fabulous (but badly indexed) Pragmatic Guide to Programming Ruby, written by the same jokers who wrote The Pragmatic Programmer (an excellent book to read if you aspire to programming).

Some parts of the language seemed fairly straightforward. Loops can be done as usual, conditionals (if-else) are normal, functions actually take a parameter list (as opposed to Perl, where they're both passed and returned through a variable called @_). I was pleased at first to see that Ruby is fully object oriented. Fully? As opposed to what? As opposed to Perl's hacked-in excuse for object oriented programming in a blatantly scripted language. Ruby's object orientation is not bad. It has a lot of features, but it also has what I think are its biggest flaws.

Ruby's concept of objects is related mostly to message-passing. When you want to invoke a member function of an object, say, make a bike shift gears, you send a message to the bike, saying, "switch to gear 5." In code-speak, myGaryFisher.changeGear(5). In strongly typed Object Oriented languages such as Java, C#, and C++, at compile-time, the compiler checks your source code to make sure that the object named myGaryFisher really does have a method called changeGear(). It won't let you run your program unless that's true.

More specifically, those languages attach a type to every object reference encountered in the program. So if you ever encounter the myGaryFisher object, it will actually be declared as Bicycle myGaryFisher a bit earlier (if anyone is thinking about polymorphism at this point, hang on; I'm getting there). This means that the compiler rigorously enforces not invoking methods on objects that it knows they do not have. If you try to call myGaryFisher.jointAround(), the compiler will check the type of myGaryFisher, see that it is of type Bicycle, then check the Bicycle class for a method called jointAround(). Of course since that method is not present in that class, the compiler will not produce machine code that tries to do that method invocation.

In Ruby, references are never declared with a type, so there is no type information checked at "compile time." Since Ruby is interpreted, this "compile time" would take place when the interpreter runs through the file and does a syntax check. Without the information that myGaryFisher is a Bicycle, all Ruby does is attempt to send the jointAround() message to the object and throw up its hands in disgust when it doesn't work. Though I believe that this kind of checking, done at compile time, can help eliminate buggy code, there's a more sinister issue behind it. After all, doing a typecast in Java always has the threat of a ClassCastException, which is the same kind of trouble.

The real trouble of this loose typing comes from the polymorphism features of the language. Here's a quick review for those of you who can't remember all those fancy OO words. myGaryFisher is a Bicycle, but if properly defined is also a Vehicle. In view of this, there are two ways in OO programming to consider the bicycle: as a bicycle, a two-wheeled thing with handlebars, a frame, foot pedals, derailleurs, a chain, gears, and everything that makes a bicycle what it is--all in all, a very specific view of the bike; or, as a vehicle, which has as its only real defining factors that it carries a certain number of passengers and that it is moving at some velocity.

In most (all?) OO languages, this is implemented via some kind of inheritance mechanism. In a strongly typed language, every time you see a reference to an object, the type determines what view of the object you have access to. If you get Bicycle myGaryFisher, because the compiler knows you have a Bicycle, you get access to a whole ton of descriptive functionality pertaining to a bicycle. If, instead, you get Vehicle myGenericPersonnelTransport, you instead have access only to a function that tells you how many people the thing can carry, and how fast it's going.

This is an information hiding tool; it allows you to give certain parts of the code access only to specific information that it is allowed to "know about." In a strongly typed language, the compiler checks this and generates code that conforms to these type restrictions. In Ruby, it does not. The only thing enforcing these type rules is you, and that, in my opinion, is just asking for trouble. When you call a function in some library, the only way you know what kind of object exactly the function is expecting is by reading the function's documentation... and we all know how keen programmers in general are about writing complete, up-to-date documentation (or can infer from the tone of this sentence).

I like the inheritance mechanism. It works well, and "mixins" are a good analog to interfaces (abstract base classes) found in Java, C#, and C++. Though I still have the benefit of code reuse through inheritance in Ruby, I lose the benefit of polymorphism. As far as I'm concerned, each of those makes up 50% of the value of inheritance. So I continue to make mixins and superclasses and things, but every once in a while I have to take a deep breath and do some type checking: this function is expecting an instance of this mixin; I must be very careful not to call any methods of the object, only methods of the mixin. I have to be cautious, because it's easy to forget, and the code will still work for the time being.

Moving on to other topics gripe-worthy, the standard library bothers me, because it appears to be hacked together quite randomly. It doesn't help that half the classes in there are lacking documentation. It doesn't help that some classes very much step on each others' toes.

For example, there is a Mutex class and also a Monitor class. The difference? The former does not allow a single thread to lock the mutex multiple times (why not?), but the latter does. The former is part of the Core API, and the latter is part of the standard libraries. I think they should be swapped.

I don't like that there isn't a Collection mixin or superclass or something, so that I can have a uniform way of adding and deleting elements from a Set, Array, and Hash.

I've always had a problem with operator overloading, and Ruby takes this to a whole new level. The | (pipe) operator for a number does a bitwise OR operation, but the same operator for an Array does a set union. Yes, these things are somewhat related, but doing this just seems like obfuscation, not convenience. Your goal should be to make code that's wrong look obviously wrong (this is one of the most brilliant programming articles I've read).

Similarly, Ruby's (and C#'s, for that matter) thing for "properties", that is, statements that look like you're setting a member variable of an object to a value, but actually are calling a function with the right-hand-side as a parameter,  bother me. I don't like deception like that. I want to know when I'm doing a simple assignment operation and when I'm calling a function. This is all about performance, isn't it? With the advent of harder, faster, stronger computers, this won't matter! Right? Wrong: what if the function that is secretly called has side effects in the object? Assignment is a relatively simple operation, and I want to know when something that I think is assignment actually is some O(n2) function.

Your response to this may be that I ought to open my eyes and read the docs, but the docs aren't there or are incomplete, half the time. I don't trust this kind of detail to documentation, only to enforcement by the language.

Despite all this, I think any of you programmers out there should learn some of this language. It allows you to do certain things very elegantly, which gives you insight about elegant ways to program in other languages you'll probably use more. Iterators and blocks (Procs), in particular, are very cool.

A Proc is basically a block of code that is stored as an object and can be passed around and used later. What's amazingly convenient about it is that the block can be returned from a function and still work. Normally, all the local variables would go out of scope once that function had returned, but a Proc always has access to all its local variables, even if those variables have gone out of scope.

I'm pleased that working with files and other I/O is just as simple in Ruby as it is in Perl. I am pleased that they have a fully function regular expression library that's integrated into the language (via /pattern/ type of expressions). I'm glad that hashtables have language support as well--for creating and accessing. It really is kind of nice that everything, including numbers, is an object. It saves me the time of considering whether I'm passing a primitive type or an object reference (Java, I'm looking at you).

Finally, I like two interesting naming conventions I've seen so far. For member functions that are queries, such as whether an object is equal to another, or whether some flag is set to true or not, the convention is to make use of a ? in the function name. For example, the equality test between two objects is Object.eql?(). Some member functions come in two flavors: one that does some processing on the object, but returns a new copy that reflects the changes; and one that directly modifies the object. For the latter, the convention is to append a ! on the function name. For example, the function that returns a new copy of a string with all the letters in losercase is String.downcase(), but one that modifies the string is String.downcase!(). It seems intuitive. The exclam alerts you that you're making a change--a potentially unsafe thing to do.

That's all for now. If I find anything else substantial, I guess I have to make a new post. Hope this was informative, and that you're all inspired to go out and try Ruby!

0 comments:

Post a Comment