Archive for the ‘ruby’ tag
DHH’s RailsConf 2010 Keynote Video
The keynote is about Rails 3.0 and the many enhancements it bring to make web application development more fun. The improvements in writing database queries (via ActiveRelation), routes, ActionMailer are really neat and I believe would make it easier for developers to get on board with Rails.
The official release will be available in a few weeks but the current version is already good enough for production use according to DHH.
Ruby 101: Make your class behave like a Ruby built-in
I got re-acquianted with this scenario while working on the OpenAmplify gem – a wrapper for the OpenAmplify API. When you give the api a text like a blog comment, it will return a list of common terms, opinion scores, named locations, and other information that can be used for text mining operations.
The OpenAmplify returns key-value pairs in an XML string by default, but it can also be in JSON, CSV, or RDF format. From a Ruby client’s point of view, we want it in Hash. You can choose to use an XML library like Nokogiri but in my opinion, working with a Hash fits nicely with Ruby.
Anyway, back to the problem. I have an instance variable that holds the data. One approach is to give clients access to the instance variable.
class Response
attr_reader :data
def initialize
@data = {}
end
end
data = response.data
topics = data[‘Topics’]
One major issue with this approach is you’re exposing the internals of your class. What if you decided to rename the variable into ‘@results_in_hash_form’? Then, all programs that uses your code will break. Worse, you will be limited from enhancing the behavior of your class like lazy loading of the data. You can wrap the access to your data inside a method but that still presents the problem of exposing the internals of your class. Also, that’s an unnecessary extra line of code :)
My suggestion is to make ‘Response’ behave like a Hash so we can do these:
topics = response[‘Topics’] response.has_key?(‘Topics’) # And still have our own methods: response.some_method_we_defined
So, how can we do this? The trick is to delegate the calls to the instance variable. One approach is to define the Hash methods you want to support:
class Response
[‘[]’, ‘has_key?’, ‘fetch’, ‘empty?’, ‘keys’].each do |method_name|
class_eval <<-EOS
def #{method}(*args)
@data.send(‘#{method_name}’, *args)
end
EOS
end
end
The code above is a shortcut to writing every method by hand. If you want to support all Hash methods, that would be a lot of typing.
A better approach is to just take advantage of Ruby’s ‘method_missing’ which is called every time an undefined method is called.
class Response
def method_missing(name, *args, &block)
@data.send(name, *args, &block)
end
end
Of course, how your ‘method_missing’ will look like depends on your requirements. In our simple case, we can simply delegate to @data.
This approach is called a “Dynamic Proxy” from the book Metaprogramming Ruby by Paolo Perrota. If you want to take your Ruby skills to the next level, I highly recommend this book.
Ruby 101: Hash initialization gotcha
I have a code that counts how many times a word occurs – a perfect fit for Hash.
def word_counts(words)
counts = Hash.new(0)
words.each do |word|
counts[word] += 1
end
end
categories = {
:a => word_counts(‘some text’)
:b => word_counts(‘another set of text’)
}
Somewhere, I use the hash returned by the word_counts method to do some calculation.
def score(word_scores, words)
words.each do |word|
v = word_scores[word]
v = 0.1 if v.nil?
score += Math.log( v / some_value )
end
end
categories.each do |category, word_counts|
score(word_counts, %w{some random text})
end
When I run the score, I always get an ‘Infinity’. After some debugging, the problem is this piece of code:
v = word_scores[word] v = 0.1 if v.nil?
‘word_scores’ returns 0 if ‘word’ doesn’t exist; not nil which is the default behavior. Later, I realized I initialized it via Hash.new(0) which makes 0 the default value. In fact, it is not even necessary to check for nil or 0. All we want is to retrieve the value referenced by the key, and if the key does not exist, give me 0.1.
v = word_counts.fetch word, 0.1
By the way, the code is from a simple exercise on Naive Bayes algorithm to classify text.
Ruby 101: How to filter an Array using proc
Over at the PhRUG, a Ruby developer community based in the Philippines, we conduct code review sessions via our mailing list. A code is posted and members share alternative implementations. So far, it’s been effective and newbies and veterans alike are learning new things in Ruby. Here’s a recent code that filters an array based on several conditions.
matches = [] (0..9).each do |i| if (i > 5) && (i % 2).zero? && (i % 3).zero? matches << i end end
The first improvement made by Bong is to use ‘select’.
matches = (0..9).select {|i| (i > 5) && (i % 2).zero? && (i % 3).zero?}
What if you are going to include another condition? Of course, you can argue to simply hard code the new condition but that wouldn’t be fun :) The solution by Tim and Neil is to use procs which I’m sure would make Matz very happy.
conditions = [
proc { |i| i > 5 },
proc { |i| (i % 2).zero? },
proc { |i| (i % 3).zero? }
]
elements = (1..10).to_a
matches = elements.select do |i|
conditions.all? { |c| c[i] }
end
puts matches.join(',')
We can even add this filtering to the Array class, just in case we need to use this across our application.
class Array
def matches(*conditions)
select do |i|
conditions.all? { |c| c[i] }
end
end
end
matches = elements.matches(*conditions)
puts matches.join(',')
matches = elements.matches( proc{ |i| i > 5 })
puts matches.join(',')
matches = elements.matches( proc{ |i| i > 5 }, proc{ |i| (i%3).zero? })
puts matches.join(',')
Ruby 101: How to add methods to a Ruby class
Let’s add a method that checks whether an Array has many elements.
a = [1,2,3]
a.many? # NoMethodError: undefined method `many?'
Let’s fix this by adding a new method to the class Array.
class Array
def many?
size > 1
end
end
a = [1,2,3]
a.many? # true
b = [1]
b.many? # false
c = []
b.many? # false
Let’s implement Rails’ fancy ‘days.ago’ method:
5.days.ago # NoMethodError: undefined method `days' for 3:Fixnum
Now, add the days and ago methods to Fixnum.
class Fixnum
def days
self * 60 * 60 * 24 # we store seconds in a day
end
def ago
Time.now - self
end
end