Wednesday, September 29, 2010

Why Ruby?

Emancipate yourself from mental slavery, none but ourselves can free our minds. --Bob Marley, Redemption Song

There are a number of reasons to love Ruby and I will share some of them here. Ruby is influenced by some of the greatest languages ever invented: Perl, probably the most pragmatic language in the world, hell, even God used it :)

Lisp from XKCD

Ruby is also influenced by Smalltalk and Lisp. Both these languages are still living, vibrant languages despite their respectable age, 40 and 60 years respectively.

When Matz chose parts from the above programming languages his primary design goal was Programmer Happiness. He succeeded.

Here are some features that I love about the language.

Parallel Assignment

You can assign many values on the same line by separating them by commas. They are assigned in parallel. No need for temporary variables.

# Swap a and b, without a temporary variable
a, b = b, a

Multiple Return Values

If you want to return more than one value in Ruby all you need to do is to wrap it in an array. Then you can get the values by using commas. Also note, that return is not required in Ruby, the last evaluated expression is returned.

def div(a, b)
  [a/b, a%b]
end

a, b = div(5, 2)
# a = 2, b = 1

String Interpolation

Double-quoted strings can be interpolated.

kid = 'Kid'
"Hello #{kid} ##{1+3}"
# => Hello Kid #4

Here documents

You have seen them in other scripting languages and they are as good as they look. No need for String concatenation or embedded newlines (\n).

sql = <<-SQL
SELECT allocations.project_id as project_id, year, week, allocations.id as allocation_id
  FROM weeks
  LEFT OUTER JOIN allocations ON allocations.start_date < weeks.end_date
    AND allocations.end_date > weeks.start_date
    AND allocations.project_id = ?
  WHERE weeks.start_date >= ? AND weeks.end_date <= ?
SQL

Higher Order Functions

Higher order functions are easily expressed with Ruby blocks.

# Map converts an array of values to a new array of values
["a", "b", "c"].map { |item| item.upcase }
# => ["A", "B", "C"]

# Zip zips arrays together
[1, 2, 3].zip([10, 20, 30])
# => [[1, 10], [2, 20], [3, 30]]

# Zip with a function applies the function to the results
[1, 2, 3].zip([10, 20, 30]) {|arr| arr[0] + arr[1]}
# => [11, 22, 33]

Simple Command Line Access

The command line is always readily available in Ruby

# Backticks invokes the command and returns the result as a string
`ls`
# => "Scripts\nStylesheets\nbin\nget_destinations.scpt\nlib\nnibs\n"
`ls`.split
# => ["Scripts", "Stylesheets", "bin", "get_destinations.scpt", "lib", "nibs"]

# System invokes the command, prints the result to stdout, and return a boolean
system('ls -l')
total 64
drwxr-xr-x  3 andersjanmyr  admin    102 Aug 29  2009 Scripts
drwxr-xr-x  3 andersjanmyr  admin    102 Aug 29  2009 Stylesheets
drwxr-xr-x  5 andersjanmyr  admin    170 Aug 29  2009 bin
-rw-r--r--  1 andersjanmyr  admin  12312 Aug 29  2009 get_destinations.scpt
drwxr-xr-x  3 andersjanmyr  admin    102 Aug 29  2009 lib
drwxr-xr-x  3 andersjanmyr  admin    102 Aug 29  2009 nibs
# => true

system('ls tapir')
ls: tapir: No such file or directory
# => false

Open Classes

Open classes mean that it is possible to add new methods to already existing classes. This is very powerful.


class String
  def vowels
    self.scan(/[aeiouy]/i)
  end
end

"A tapir is beautiful".vowels
# => ["A", "a", "i", "i", "e", "a", "u", "i", "u"]

There are two common reactions to this.

  • Wow! This is really cool, this will allow me to put everything in the right place.
  • What! That is too dangerous, our programmers will create an unmaintainable system.

I'm in the first camp!

Class Inheritance

In Ruby the classes are objects and follow the inheritance rules of objects. No statics are needed.

class Mammal
  # This is a class method, that returns all mammals of this kind
  def self.all
    mammals.select do |m|
      m.kind_of? self
    end
  end

  # Another class method that gives access to the class varibable
  def self.mammals
    # @@ is a class variable prefix
    @@all_mammals ||= []
  end

end

class Tapir < Mammal
end

# Add some mammals and a tapir
Mammal.mammals << Mammal.new << Mammal.new << Tapir.new

Tapir.all
# => [#<Tapir:0x000001010fc1a0>]

Mammal.all
# => [#<Mammal:0x000001010fc218>, #<Mammal:0x000001010fc1c8>, #<Tapir:0x000001010fc1a0>]

Meta Programming

When the classes are parsed, Ruby is already running and since it is possible to create methods and classes on the fly you can do some very powerful meta-programming with Ruby. Here is a simple example.

class Tapir

  [:sniff, :eat].each do |method_name|
    send :define_method, method_name do |thing|
      "I'm #{method_name}ing #{thing}!"
    end
  end

end


t = Tapir.new
t.eat 'bananas'
I'm eating bananas!

t.sniff 'glue'
I'm sniffing glue!

This type of meta-programming is what enables the simplicity of Rails' has_many, and belongs_to methods for declaring relationships.

Method Missing

The last feature of Ruby that I want to share is method_missing. method_missing is a feature that Ruby inherited from Smalltalk. If an object is invoked with a method that is not defined by its class or ancestors, method_missing(method, args) is called instead. By implementing this method you can effectively create all sorts of cool stuff such as proxies, prototypes, etc.

# Simple implementation of a Javascript-like structure without inheritance
class Prototype

  def props
    @props ||= {}
  end

  def method_missing(method, *args)
    m = method.to_s.sub('=', '') # Strip the trailing '=' to allow setters
    if args.empty?
      props[m]
    else
      props[m] = args[0]
    end
  end
end

prot = Prototype.new
prot.age
# => nil
prot.age 14
# => 14
prot.age
# => 14
prot.age= 16
# => 16
prot.age
# => 16

Inside method_missing it is of course also possible to define new methods and classes dynamically, but I'll leave that for another day.

Ruby is a supreme dynamic language that lets you do almost anything. Try it!

When you can do anything, you have to become more responsible. You own your fate. And, that, I think, is a situation which promotes responsibility. -- Michael Feathers, author of Working Effectively with Legacy Code

No comments: