Sunday, December 11, 2011

Learning Ruby, and Ruby vs. Lisp

The company I work for has a lot of legacy Ruby code, and as Ruby has become kind of a mainstream language, I decided to get a book about it and learn how it works.

As my learning resource, I chose The Ruby Programming language by David Flanagan and Yukihiro Matsumoto as that receives great customer reviews, covers Ruby 1.8.7 and 1.9 and is authoritative because the language creator is one of the authors.

The book makes a good read in general. There are plenty of code examples, but not too much to obscure the prose. What I found first interesting, later annoying, was the frequent use of words like "complex", "complicated", "confusing", "surprising" or "advanced" to describe features of the language. I'd rather decide myself about using such attributes to describe something that I've just learned.

Having spent so much time with Common Lisp, I almost forgot that programming languages usually evolve over the years. Ruby is no exception, and the fact that there are significant differences between Ruby 1.8.7 and Ruby 1.9 kind of bothers me - I'll probably never write code in Ruby 1.8.7, but the differences between the two versions seem to be rather subtle and I'm curious to see how much that is going to be a bother in the future, working with legacy code.

The common theme for Ruby seems to be succinctness. This comes at the expense of making the syntax rather complex, with several special case rules required to solve ambiguities. I don't have the practice to judge whether this is a problem, but from the book, it seems there are quite some things to remember.

It seems that Ruby started off as a purely object oriented language and only later discovered that function-oriented programming is nice, too. The deep roots of object orientation made it rather hard to actually get free functions (which are not member functions of an object) integrated. Contrary to what I am used to, member functions are not a special case of free functions, but rather something quite different. It requires explicit conversion steps to convert a member function into a free function (called procs in Ruby), and invocation syntax is also different between the two. Again, the description may sound worse than it is in practice.

What I really liked was the generalization of code blocks into fibers. Ruby does not have full coroutines, but the restricted form that is available is generalized well and seems like it could be useful for building pretty wild asynchronous systems. Also, it is nice that the bindings of closures can be accessed.

But then, Ruby is an interpreted language and this fact is re-stated throughout the book. With Just In Time compilation, this could become a non-problem, but I'm not sure how well Ruby can be optimized due to its very dynamic nature. Just to see how fast it is compared to Common Lisp, I implemented the Sudoku solver from chapter 1 of the Ruby book in CL and gave the two implementations a puzzle to solve. It took the Ruby solver 0.890 CPU seconds (Ruby 1.9.2p290), whereas the Lisp solver (Clozure CL 1.7) used 0.087 CPU seconds to solve the puzzle. Ten times slower, whatever you'll make of that.

In the book, it is mentioned how little code the Sudoku solver actually uses. This is true, but then, the Lisp version is not longer. It does not seem as if adding syntax is actually the best way to add the possibility to write succinct programs to a language, and the price of the complex grammar is rather high.

Writing the CL solver, I found myself not writing tests again and then poking around in problems of my implementation without knowing what works and what does not. As I want to practice more TDD, I stepped back and added tests. This led me to solve a problem that I had with my previous attempts to practice TDD in Lisp - I do not want to export all the symbols that the tests exercise from the packages that I use, but I also don't want to import the unit testing library into my own library packages. Thus, I wrote a deftestpackage macro that creates a new package to contain the tests that I write and automatically imports all symbols from the package being tested. That way, I can easily keep tests and library source separate and don't need to qualify internal symbols in the tests.

My overall takeaway on the Ruby is this: Ruby seems to be a language that has grown from being purely object oriented to supporting functional programming. That growth was not completely natural, and it seems that if Ruby is not used as a pure object oriented language, the syntax becomes rather messy and hard to grasp. This is similar to C++, which in its first versions was relatively nice (I hear you "ow"!), but has grown into into an incomprehensible mess once people recognized how templates can be abused for metaprogramming.

I can see the appeal of Ruby, but there seems little it has to offer to me that Common Lisp cannot provide. The lack of a formal specification and the ugly grammar put me off. Then again I'm pretty sure that Ruby is more enjoyable than many other popular languages. I'm looking forward to see my theoretical conceptions be shaken by actual practice.

Share:

8 comments:

  1. what it can offer you is rails and employment. technically it isn't great.

    ReplyDelete
  2. re test package and symbols: my answer to that is hu.dwim.common:import-all-owned-symbols.

    http://dwim.hu/darcsweb/darcsweb.cgi?r=HEAD%20hu.dwim.common;a=headblob;f=/source/common.lisp#l51

    (which should really be called import-all-present-symbols. /me goes and renames.)

    ReplyDelete
  3. I'd somewhat agree with your point about Ruby syntax; it's terse and expressive but there's a fair bit of it to learn for the uninitiated. That doesn't concern me too much- going from a lisp dialect -> Ruby homoiconicity is what I really miss, but I could say that for OCaml, Java, etc too. I'd dispute the assertion that Ruby has gained FP as an afterthought: traditional functional methods for manipulating collections, lambdas etc are pretty engrained in the language. MRI still even has callcc, a hangover from its early Scheme influences.

    ReplyDelete
  4. > there are significant differences between Ruby 1.8.7 and Ruby 1.9 kind of bothers me

    You think that's bad, wait till you see the differences between Rails 0.12 and 3 ;-)

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. I must say that for system programming, ruby has become my go-to language. It's really not very well-implemented in MRI, and there are many alternative rubies out there, but they all seem to be implementing 1.8 for now, so... yeah. Threading sucks, almost any sort of I/O is dreadfully slow, and if you want to get sub-100ms response times in non-ridiculous web pages, forget it. But then there's the programs it lets you write.

    It's really weird, but programs I write in ruby come out very clear and concise. It really allows you to write nice little DSLs, if you decide to express your program in the way ruby wants you to express it. And often enough, that works very well!

    And then you run tests, and even loading the test suite and your library takes half a minute. You can tell I have a love/hate relationship with this language (-;

    ReplyDelete
  7. Ruby helped me a lot. Before Ruby, writing code was painful. With Ruby I was allowed to experiment faster, to bring more programs and libraries to fruition faster, to see what worked and what didn't after those experiments...
    I wasn't able to trust Ruby all the way, though. Sometimes I distrusted the language but learned that the problems I was seeing were of my own making. Given how extraordinary Ruby is, we don't have a full grasp of it just from writing some code in it.
    To put some of that into words, trusting the modularity of Ruby can be troublesome. It starts from Ruby code being executable code and how easy it is to require some other file. Perhaps the modularity of Ruby is too fine-grained. Perhaps having too much executable code can be a little disconcerting.
    For all of the perceived shortcomings of Ruby, I've found a good alternative in the new-to-be Dart programming language. Like Ruby, it has great OO support. But it goes beyond Ruby in having a more coarse-grained modularity, in avoiding too much executable code when declaring classes and so on, and in some ways in having a more standard syntax as well.
    With Dart, "for is back, baby!"
    Both Ruby and Dart lend themselves great to creating accessible APIs. Ruby will continue to be easier to use: IO.read('some_file.txt'); Dart has more of an enterprise and mainstream feel to it that will always hamper it compared to Ruby. That said, Dart could grow more popular than Ruby in the not-too-distant future. If Dart lands on the Google App Engine and on the Google Chrome. We'll see.

    ReplyDelete
  8. Programming language demonstrations love to show off the language's brevity compared to other languages.

    Erlang the Movie
    http://video.google.com/videoplay?docid=-5830318882717959520

    For some comparisons, such as Erlang and C, there really is a difference in average code length. But code length isn't the problem. Custom functions, macros, and short variable names bridge the gap. The more important distinction between languages is expressiveness, something that can't always be measured in SLOC.

    Java has no lambda, which is made more embarrassing by the fact that C++ now has one, and Java was originally designed as an improvement on C++. The lambda has no syntactic equivalent: either a language has anonymous, manipulable functions, or it doesn't. True, an individual lambda can be rewritten as a hard-coded function, but it cannot be composed or curried like a lambda. For any particular code comparison, lambda may seem like it saves a few SLOC, but the benefit isn't concision, it's raw POWER. Once you've been bitten, there's no going back. A language without lambda is like a language without switch statements. For any particular switch, you could rewrite in terms of nested if statements, but it's agonizingly boilerplate and unproductive.

    ReplyDelete