Following the advise from
Debasish Ghosh, I put my list of new languages to learn and one of them was Ruby.
Why Ruby?
- Many web developers have told me great deals about the language and aboutRails.
- Puppet is a system management that I want to learn and it uses Ruby.
- Some of my pending books use Ruby for its examples - Programming Amazon Web Services
- I have a few projects in my pipeline that requires a lot of code with basic funcionality (CRUD) and a nice GUI
- I want to avoid the "gold hammer" antipattern
Questions that I had up-front:
- Is the syntax that nice? EVERY SINGLE Ruby guy swears by the syntax. What is so sexy about the syntax?
- How slow is it? I have been hearing a lot of rumors regarding the horrible GC of Ruby. Java programmers have blog that JRuby was faster than Ruby
- Multithreaded - green threads? Heard a lot from colleagues that Ruby is not well equipped for multithreaded
- How long would it take me to learn it? Try to measure the learning curve.
- Where would I use this in real life? Where are the best places/projects that I could use Ruby and most important which ones to avoid (Twitter).
- How does it compare to Java? I'm sure that there are some pros and cons regarding the syntax, but what is difference of using Groovy, JRuby, or Scala vs using Ruby?
AnalysisSyntaxIndeed, the syntax is very nice. It is very similar to Groovy. It does has a few features in the language that I haven't seen in others. Here are some examples:
Virtual Attributes
I created a BookInStock class along with a few required parameters, then I wanted to get the price in cents.
class BookInStock
attr_reader :isbn, :price
attr_writer :price
def initialize(isbn, price)
@isbn = isbn
@price = Float(price)
end
def price_in_cents
Integer(@price*100 + 0.5)
end
def price_in_cents=(cents)
@price = cents / 100.0
end
end
book = BookInStock.new("isbn1", 33.80)
puts "Price = #{book.price}"
puts "Price in cents = #{book.price_in_cents}"
book.price_in_cents = 1234
puts "Price = #{book.price}"
puts "Price in cents = #{book.price_in_cents}"
Result:
Price = 33.8 Price in cents = 3380
Price = 12.34
Price in cents = 1234
Here we've used attribute method to create a virtual instance variable. To the outside world, price_in_cents seems to ben attribute like any other. Internally, though, it has no corresponding instance variable.
Default HashLets say that you need to count the amount of words in a file. The way that would do the counting, is that each line would be treated like an array of words. Create a hash, check if the hash has the word, if it doesn't then add the count of 1; otherwise, increment it.
if(counts.containsKey(word))
counts.put(word, counts.get(word) + 1);
else {
counts.put(word, 1);
}
In Ruby you can create a Hash.new(0), the parameter (0 in this case) will be used as the hash's default value - it will be the value returned if you look up a key that isn't yet there.
def count_frequency(word_list)
counts = Hash.new(0)
for word in word_list
counts[word] += 1
end
counts
end
Regular ExpressionYou can give name to the pattern and retrieve the value of the matched regular expression
pattern = /(\d\d):(\d\d):(\d\d)/
string = "It is 12:34:56 precisely"
if match = pattern.match(string)
puts "Hour = #{match[:hour]}"
puts "Min = #{match[:min]}"
puts "Sec = #{match[:sec]}"
end
The result is the following:
Hour = 12
Min = 34
Sec = 56
LambdaLambda is like a call done assuming that it is like a method:
Form example:
say_hi = lambda {|a| "Hello #{a}"}
say_hi.("quintin")
Will return, "Hello quintin"
Another example, lets say that you have to calculate ten consecutive numbers but the calculation is based on a parameter passed by the user (multiplication or addition).
if operator == :multiplication && number != nil
calc = lambda{|n| n * number}
else
calc = lambda{|n| n + number}
end
puts ((1...10).collect(&calc).join(","))
Here, line 7 does the iteration of each of the 10 numbers and uses the appropriate calculation based on the parameter and passes each number to it. Then, it joins each number with a comma.
Slow?The Ruby team has done a lot to its virtual machine, but (coming from a static typed language perspective) it is still hugs a lot resources.
MultithreadedIn the previous version of Ruby (1.8), the way that it handle the threads was through its VM. This process is called
green threads. In Ruby 1.9, threading is now performed by the operating system. The advantage is that it is available for multiprocessors. However, it operates only a single thread at a time.
Learning Curve
If you are using Groovy, Python, or any similar dynamic language, the learning curve is almost null. The only problem is the lack of good support for editors. NetBeans used to be a very nice editor, but they have decided to discontinue the support for Ruby. I've been a Mac guy for quiet some time now, so I've been using TextMate and it worked great.
Where should I use it?
I try to "pigeon hole" the applications carefully before me or my team decide which type of language to use. For example, if the application needs to be highly efficient with a large amount of transactions, and performance needs to be immediate, I do NOT trust Ruby (sorry). I would turn to Java, Spring, Hibernate/iBatis, EHCache stack. However, if the application is a quick and simple CRUD pages with low usage like a series of admin pages, then Ruby would be my choice.
Comparing Ruby to JavaIt is worth to keep Ruby in your toolbox in case you need to do quick scripts or sites. I did end up learning different things, like Ruby's nice components. For example, if you would like to get the top-five words in the previous word count example, you can just do the following:
sorted = counts.sort_by {|word, count| count}
top_five = sorted.last(5)
The sort_by and inject are two handy component. Here are some other examples:
[ 1, 2, 3, 4, 5 ].inject(:+) # => 15
( 'a'..'m').inject(:+) # => "abcdefghijklm"
Also, being a
TDD guy, I really enjoyed the
Behavior-Driven Development or BDD. Based on the books and forums that I've read, this is the Ruby community's choice of tests. It encourages people to write tests in terms of your expectations of the program’s behavior in a given set of circumstances. In many ways, this is like testing according to the content of user stories, a common requirements-gathering technique in agile methodologies. Some of the frameworks are
RSpec and
Shoulda. With these testing frameworks, the focus is not on assertions. Instead, you write expectations.
The class that I created is a class for tennis tournament:
class TennisScorer
OPPOSITE_SIDE_OF_NET = {:server => :receiver, :receiver => :server}
def initialize
@score = { :server => 0, :receiver => 0 }
end
def score
"#{@score[:server]*15}-#{@score[:receiver]*15}"
end
def give_point_to(player)
other = OPPOSITE_SIDE_OF_NET[player]
fail "Unkown player #{player}" unless other
@score[player] += 1
end
end
The test is the following:
require 'simplecov'
SimpleCov.start
require_relative "tennis_scorer"
describe TennisScorer, "basic scoring" do
let(:ts) { TennisScorer.new}
it "should start with a score of 0-0" do
ts.score.should == "0-0"
end
it "should be 15-0 if the server wins a point" do
ts.give_point_to(:server)
ts.score.should == "15-0"
end
it "should be 0-15 if the receiver wins a point" do
ts.give_point_to(:receiver)
ts.score.should == "0-15"
end
it "should be 15-15 after they both win a point" do
ts.give_point_to(:receiver)
ts.give_point_to(:server)
ts.score.should == "15-15"
end
end
As you can see you add context to your test and check if the solution is fine. Executing the tests echoes the expectations back and validates each test.
How slow is it?Dave Thomas, author of the book
Programing Ruby 1.9 version 3 wrote the following in his
post regarding the re-factoring of the code of Twitter from Ruby to Scala:
At the kinds of volumes that Twitter handles (and with what I assume is a somewhat scary growth curve), Twitter needs to improve concurrency—it needs an environment/language with low memory overhead, incredible performance, and super-efficient threading. I don't know if Scala fits that particular bill, but I know that current Ruby implementations don't. It isn't what Ruby's intended to be. So the move away is just sound thinking. (I suspect it also took some courage.) I applaud Alex and the team for this.
Instead of defending Ruby when it's clearly not an appropriate solution, let's think about things the other way around.
The good folks at Twitter started off with Ruby because they wanted to get something running quickly, and they wanted to experiment. And Ruby gave them that. And, what's more, Ruby saw them through at least two rounds of phenomenal growth. Could they have done it in another language? Sure. But I suspect Ruby, despite the occasional headache, helped them get where they are now.
ConclusionFinally, I would recommend learning Ruby and I would definitely keep doing more stuff with it. Although there are some characteristics similar to Groovy, Python, and others dynamic languages, it does has some nice different features. Also, the Ruby community is very vibrant/active. There are thousands of programmers building packages/APIs or gems.
The way to load the API is very similar to the "
yum" command in Linux. Let say you need to do the following:
- Connect to a GMail account
- Check for e-mails that have attachments
- Do some type of business logic
- Send an e-mail with your results
I could create everything from zero, but instead I was able to find a nice little gem named "gmail". I just did "gem install gmail" and voila, I got the API! I also wanted to use MongoDB for a Ruby on Rails (RoR) project and a
podcast explain to me how to do it.
Again, this is just the beginning but it was a really nice experience and remind me back of why I got into programming. The challenge and the unknown is what drive must of us on finding better solutions.