Expand Cut Tags

No cut tags
topaz: February 20, 2008 (lunar eclipse)
[personal profile] topaz
[livejournal.com profile] khedron asked me after my last Ruby post what I thought of the language, and whether I agreed with the sentiment that Ruby can be a pretty decent substitute for Lisp in a lot of cases.

Ruby is a nifty little language that's had a small but devoted following for about 15 years.  It's gotten a lot of attention recently with Ruby on Rails, a Web development framework that implements the model-view-controller design pattern for Ruby applications.  Some things it does very nicely: its syntax is more highly reflective than any other language I can think of offhand since Smalltalk.  At some point I should write something about the cooler parts of Ruby, but right now it's annoying me and so I'm afraid that today you get the bile.

There is always the risk, when writing an article like this one, of criticizing something simply because it is different from something that you happen to like more.  I am sure this is no exception.  I will try not to criticize Ruby for not being Lisp, or C, or heaven help me, Perl, but on the basis of its own merits.

First-class functions

Many methods in Ruby are accompanied by code "blocks" where other languages might use a closure or a reference to a subroutine, e.g.:

values.sort_by { |x| x.abs }
palindromes = words.find_all { |w| w == w.reverse }

The confusing and maddening thing here is that you can't pass a function or a reference to one.  Why not?  Well, because it's not a block, and these methods will only accept a block.  What's a block?  A block is a chunk of executable code.  You mean... like a function?  Right.  I mean, actually no, not really.  What?

Trying to resolve this issue can reduce even the bravest warrior-hackers to a gibbering, cowering mess.  [livejournal.com profile] zsquirrelboy found me a very good analysis of Ruby's handling of first-class function objects by Eli Bendersky, who does an excellent job of describing the differences between Ruby's methods, Procs and blocks.  Anyone else who has struggled with this issue should go read his commentary right away.

Matz confirmed in a Ruby article in InformIT that this "block" construction is a syntactical oddity. It comes into play only when the interpreter sees an open brace (or the do keyword) supplied on the same line as an iterator method. Although it looks as though you are creating an anonymous procedure object and passing it as an argument to the iterator, the block isn't really any kind of first-class object and is not delivered to the iterator via standard argument-passing mechanisms.  According to Bendersky, the interpreter quietly transforms the block into a closure and attaches it to a special slot on the iterator method, which the iterator can invoke with the yield statement.

This technique of invoking iterator methods with blocks is convenient but I find it unsatisfying and unsettling when examined more deeply.  One of the commenters on Bendersky's essay asked, quite reasonably, why it's necessary to do this:

func = lambda { |x| x + 5 }
func.call(2)

rather than the conceptually simpler

func = lambda { |x| x + 5 }
func(2)

The answer is apparently that, because Ruby puts methods and local variables in separate namespaces, and permits methods to be called without parentheses, an expression like "func 2" would have an ambiguous parse tree.  So you're not allowed to do it.

Alan Perlis once famously said that "syntactic sugar causes cancer of the semicolon."  At long, long last, I think I finally understand what he meant.

Inconsistent variable scoping

Ruby variables are declared only in the arguments to Procs or methods. When a new free variable is instantiated, it is assigned to the innermost closure active at run time.  Example:

def foo
  result = 0
  (0..5).each do |n|
    result = n
  end
end

Here exactly one result variable is instantiated across the entire method, and after the each loop terminates, its value is 5. However, in this example:

def foo
  (0..5).each do |n|
    result = n
  end
end


the scope of the result variable is limited to the each block, because --- and this is important --- Ruby converts this block to a closure when the each method is invoked (see above). That is important because it explains why this other, semantically equivalent, code behaves differently:

def foo
  n = 0
  while n < 5 do
    n = n + 1
    result = n
  end
end

See the difference?  Because the block in the while loop isn't a closure, the result variable here belongs to the scope of the foo method. It is like the first example, except that in that case we had to force the variable's scope by deliberately instantiating it at the method level. That's not necessary here because the while block isn't a closure; only the each block is.

If you are anything like me, at this point you have already begun drinking heavily.  Byzantine scoping rules like these are just a recipe for disaster.

Matz deserves a lot of credit for recognizing how bad this is.  I have not had a chance to review what's happening with Ruby2, but the changes Matz describes in these slides look like a considerable improvement from the current state of affairs.

Anyway

Despite all this I can't really say that I dislike Ruby.  I've been working in Ruby on Rails for only a few months, and have not really delved deeply into what the language has to offer, but I find a lot of its self-redefinition features really encouraging and intriguing.  I can see that Matz has tried hard to strike a good balance between conceptual elegance and programming expressiveness, and that's a difficult line to walk.  What makes me unhappy is where the language appears to violate the principle of least surprise; if issues like these can be remedied effectively, it will make Ruby substantially more appealing to hackers like me.

Date: 2008-03-10 12:14 pm (UTC)
From: [identity profile] dbang.livejournal.com


I don't know Ruby even a tiny bit, so I can't comment on most of your post, but this criticism caught my eye.

The confusing and maddening thing here is that you can't pass a function or a reference to one. Why not?

Is the ability to pass a function/reference to a function a desirable trait?

I always thought of that as one of those arcane things that C and C-like languages let you do to shoot yourself in the foot and several other body parts...and/or to ensure you have a job forever because that critical piece of code you wrote is completely unintelligible to anyone but you, and if they fire you the entire company will go up in flames of spaghetti.

Highly structured languages like Pascal certainly don't let you pass function references. Nor highly OO languages like Java. (Although of course they do let you pass an object and therefore refer to the object's methods.)

Date: 2008-03-10 12:15 pm (UTC)
From: [identity profile] dbang.livejournal.com
(It's been soooooo long since I've learned a new language. I miss it. *sigh*)

Date: 2008-03-10 01:03 pm (UTC)
ext_86356: (2632)
From: [identity profile] qwrrty.livejournal.com
Is the ability to pass a function/reference to a function a desirable trait?

Oh golly yes. For a functional language it's really a requirement. It lets you do things like:

case user_prefs("sort-by")
  when "name":
    radix_func = lambda { |a,b| a.name <=> b.name }
  when "dob":
    radix_func = lambda { |a,b| a.dob <=> b.dob }
  when "hair-color":
    radix_func = lambda { |a,b| a.haircolor <=> b.haircolor }
end

sorted_people = people.sort_by &radix_func

When your language makes a function a first-class object (a quantity that you can assign to variables or store in data structures the same way you would any other quantity like a string or an integer), this kind of idiom quickly becomes very natural. I'm not sure I'd say that Ruby is "a functional language" but it certainly inherits enough functional traits (like functions as first-class objects and a "lambda" function which is used to create them) to feel like one.

You're right that doing this in C is a good way to make your application blow up, but that's partly because the way you implement this in C is with pointers to functions, which gives the compiler no good way to check the type or number of arguments you're passing to your dereferenced functions, so it kind of has to say "I hope in hell you know what you're doing", close its eyes and jump. A decent dynamically typed language doesn't suffer from those shortcomings and makes it a lot easier to build code like this.

Date: 2008-03-10 01:07 pm (UTC)
From: [identity profile] dbang.livejournal.com
Okay, that makes sense.

The last functional (which I assume you mean in contrast to object oriented) language I used was C, in which this would be a total nightmare. (Reason #4591 to avoid C, eh?) And in Java and other OO languages, this is a non-issue...you'd just operate on your object using whatever methods it provided to do so.

So...why DOESN'T Ruby allow it?

Date: 2008-03-10 01:50 pm (UTC)
ext_86356: (Default)
From: [identity profile] qwrrty.livejournal.com
The last functional (which I assume you mean in contrast to object oriented) language I used was C

Well, no, what I mean by "functional language" is, well, a functional programming language (http://en.wikipedia.org/wiki/Functional_programming). Languages like C and Pascal are usually described as "procedural" or "imperative" (http://en.wikipedia.org/wiki/Imperative_programming) languages. The world is not divided into "Java" and "everything else," much though Sun may want us to think so. :-)

And in Java and other OO languages, this is a non-issue...you'd just operate on your object using whatever methods it provided to do so.

I'm not sure I get that it's a non-issue. My ignorance of Java is showing here, but it seems as if Java 2's Array sort method (http://java.sun.com/j2se/1.4.2/docs/api/java/util/Arrays.html#sort(java.lang.Object[],%20java.util.Comparator)) can take a "Comparator" as an argument, which looks like how you might implement the example I gave here. It's not clear to me exactly what a Comparator is (the docs seem to say that it's an "interface"), but the point is that the object itself doesn't necessarily know how you might want to sort it at any given moment; you have to provide a different thing (the comparator) that has that knowledge.

So...why DOESN'T Ruby allow it?

Didn't you read my post? :-) Doing so would interfere with Ruby's syntactic sugar that allows you to call methods without parentheses.

Date: 2008-03-10 02:17 pm (UTC)
From: [identity profile] dbang.livejournal.com
Comparator. Blech. Very un-javalike.

The reason I said it is a non-issue is because there are other more typical ways to handle this. Not that you *can't* have a class that exists solely as a reference point to methods, but that more often you would see a class that defines a method so that the object knows how to compare itself to something else. (ie: the object would implement the Comparable interface (http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Comparable.html)).

Date: 2008-03-10 04:28 pm (UTC)
From: [identity profile] khedron.livejournal.com
Un-Java-like? How so? The ability to define a local instance of a Comparator which understands local state, which you can pass off to a sorting function to sort things in a customized way, is very Java. That's been around since the beginning.

You're certainly right, there are other ways to do it. But this lets you handle arbitrary stuff.

Where C passes function pointers, Java passes objects implementing an expected interface, which may or may not have been defined on the fly. Java anonymous classes aren't quite as good as Lisp closures, mostly because they can only close over "final" variables, but they add a measure of convenience (and reduce, for once, the amount of code you have to type to get that functionality).

Date: 2008-03-10 04:31 pm (UTC)
From: [identity profile] khedron.livejournal.com
Is the ability to pass a function/reference to a function a desirable trait?

I always thought of that as one of those arcane things that C and C-like languages let you do to shoot yourself in the foot and several other body parts...and/or to ensure you have a job forever because that critical piece of code you wrote is completely unintelligible to anyone but you, and if they fire you the entire company will go up in flames of spaghetti.


I responded to this a bit below already, but Java OO code uses interfaces for (mostly) the same reasons that C would let you pass function pointers. There are situations where it's better to figure out what the right thing to do is during run time, not at compile time.

Date: 2008-03-10 05:57 pm (UTC)
From: [identity profile] enf.livejournal.com
Pascal actually does have procedure parameters. I think they may have fallen out of the public eye because (it looks like) UCSD Pascal didn't implement the feature.

Date: 2008-03-10 06:17 pm (UTC)
From: [identity profile] dbang.livejournal.com
Really? I had no idea.

Of course I haven't used Pascal in...erm...22 years.

Amen

Date: 2008-03-10 01:38 pm (UTC)
From: [identity profile] faulkner.myopenid.com (from livejournal.com)
You have the exact same set of gripes that I have. I had forgotten about the scoping issue until you mentioned it. Ugh. So much hope pinned to Ruby 2.

And dbang: C is not a functional language, it's a procedural language. When qwrrty says "functional" he means this (http://en.wikipedia.org/wiki/Functional_language).

Date: 2008-03-10 02:20 pm (UTC)
From: [identity profile] dbang.livejournal.com
Tangentially, have you looked into Groovy (http://en.wikipedia.org/wiki/Groovy_(programming_language))?
I know nothing about it personally, but I met a guy recently who was waxing evangelistic about it rather vehemently, saying that he had been a big Ruby on Rails fan, but had totally converted to Groovy, saying it solved all Ruby's flaws. I had nothing to add to the conversation, of course, knowing neither Ruby nor Groovy, but it did sound, um, groovy.

Date: 2008-03-10 02:53 pm (UTC)
ext_86356: (OMGWTF)
From: [identity profile] qwrrty.livejournal.com
Nifty! No, I hadn't heard about it. It's not clear to me from reading that how it's supposed to supplant the Rails application framework but I'll see about investigating it a little further.

Date: 2008-03-10 03:19 pm (UTC)
ext_86356: (human dalek)
From: [identity profile] qwrrty.livejournal.com
Oh, I see: Grails. (http://grails.codehaus.org/) Huh. OK. I should take a look.

Date: 2012-05-31 03:09 pm (UTC)
From: [identity profile] feoh.livejournal.com
Groovy/grails are *especially* worth a look if your code base is currently Java.

I've seen it used to incredible effect in Java shops for writing tests, adding a scripting framework, and generally offering a more dynamically typed, less verbose programming experience than pure Java provides.

Date: 2008-03-10 05:04 pm (UTC)
From: [identity profile] quigleydoor.livejournal.com
Thanks for your insights. I played around with Ruby on Rails enough to be dangerous, in John Norman's class at Harvard Extension (http://e168f07.7fff.com/about/) last semester. It is fun—my scripting tool of choice for one-off tasks like parsing a text data file. I wish the class got into issues like you are describing, instead of focusing on the Rails bag o' tricks. If it had, it would have been a great class on language design.

One thing that bugs me is that there is no convenient way to iterate through two or more collections at once. I had a problem where I had two lists of numbers, one in a file and one in a database table. I had to generate a pairs from one number from each list. This was just test data, so I didn't need to care what order it worked in or how they were paired, and randomly would have been the best way. Given how easy it is to iterate through one collection, I was bummed when this task actually slowed me down. I ended up with a SQL hack to do the pairing in the database.

Date: 2008-03-16 11:42 pm (UTC)
From: [identity profile] numignost.livejournal.com
It looks like Ruby 1.9 will permit normal enumeration like the following -- haven't updated my system to try it out though.

def pairs( seq1, seq2 )
loop do
yield seq1.next, seq2.next
end
end

lst1 = 9.downto(1)
lst2 = 20.downto(12)
pairs( lst1, lst2 ) { |x,y| print "#{x},#{y}\n" }

EDIT: I was still curious, so I installed MacRuby which is a 1.9 port. Seems to work as expected.






Edited Date: 2008-03-16 11:51 pm (UTC)

Date: 2008-03-17 12:03 am (UTC)
ext_86356: (thinky)
From: [identity profile] qwrrty.livejournal.com
OK, now that is wicked cool.

Date: 2012-05-31 03:11 pm (UTC)
From: [identity profile] feoh.livejournal.com
This is one of the most cogent and well written critiques of the Ruby language I have ever seen. I'm an unabashed fan and I think I just learned something important about the language, as well as gaining a new understanding of the criticisms some of the programming purists have been too lazy to fully articulate.

Thank you!

May 2018

S M T W T F S
  12345
6789101112
13141516171819
20212223242526
27282930 31  

Most Popular Tags

Style Credit

Page generated Jul. 6th, 2025 05:39 am
Powered by Dreamwidth Studios