Rubies in the Rough

This is where I try to teach how I think about programming.

1

MAR
2012

The Right Ruby Mix

Ruby is a melting pot language. It borrows ideas from many things that came before. It combines several different programming philosophies.

This aspect of the language can be a plus. It means that Ruby is suited to multiple applications. It also opens up some pragmatic shortcuts. Even better, it sometimes encourages us to think about problems using a different lens of thought.

Of course, this cuts both ways. Ruby living at the intersection of many ideas does have some downsides. First, there's more to learn than you find with some simpler languages. There's a cost for the extra knowledge we have to track. Even worse though, in my opinion, is that it's sometimes hard to know exactly what Ruby's style really is.

Going Off Script

One culture Ruby borrowed heavily from is that of the so called "Scripting Languages." The main source of these features was Perl, in my opinion, but you can also find influences from Bash and other sources. I found this comforting since I came to Ruby from Perl, but the truth is that it bothers some people.

What's up with that test() method, really?

One thought is that Ruby has it to make Bash programmers more comfortable. In other words, this Bash code:

$ if [[ -e "Gemfile" ]]; then echo "This project uses Bundler."; fi
This project uses Bundler.

can be ported to similar Ruby code:

$ ruby -e 'if test ?e, "Gemfile" then puts "This project uses Bundler." end'
This project uses Bundler.

That's not a very Rubyish way to check for a file though. Typically you would see it written more like this:

$ ruby -e 'puts "This project uses Bundler." if File.exist?("Gemfile")'
This project uses Bundler.

That's shorter and it reads better, so it's no surprise that Rubyists prefer it. We generally love code that reads well.

The interface to test() is still a little bizarre though, don't you think? I guess we wanted something close to -e and Ruby's character syntax ?e was deemed close enough. The truth is that we could get closer to the Bash syntax, if we really wanted to:

$ ruby -r ./bash -e 'if bash_test [[ -e, "Gemfile" ]] then puts "..." end'
...
$ cat bash.rb
%w[A b c C d e f g G k l M o O p r R s S u w W x X z].each do |arg|
  flag = -arg.getbyte(0)
  if arg.downcase == arg
    Object.class_eval do
      define_method(arg) { flag }
    end
  else
    Object.const_set(arg, flag)
  end
end

def bash_test(*args)
  test(*args.flatten)
end

Why didn't Ruby go this far? I assume the reasons are pretty obvious, but let's discuss them briefly.

First, it pollutes the top level namespace with several methods and constants. This already bit me in the creation of this tiny example. I had a bug in my first attempt at this code and I tried to debug it with p() only to realize that I had replaced that method with a much less helpful chunk of code. (If you ever run into that problem, you can still get to it with Kernel.p() thanks to the magic of module_function().) The added methods and constants return almost senseless values, so it's unlikely they would be good for much else.

Another problem is that I couldn't support all of the operators test() accepts using this trick. It also understands ?-, ?=, ?<, and ?>.

We can see that test() was a compromise, to get close to some Bash-like support without doing too much damage to Ruby itself. Was it worth it? Tough call. I favor methods like File.exist?() and the File::Stat module when I do checks, because I think they better reveal my intentions and they don't make the code a lot longer.

Now, I do use several of the borrowed Perl shortcuts. You probably know that, since I have written about them before. I find that using Ruby on the command-line with -n or -p, $_, and the flip-flop operator does save me a lot of code and time. In those cases, the tradeoff feels worth it to me.

But perhaps this just shows that I was more of a Perl guy than a Bash guy when I came to Ruby.

Do I use these crazy shortcuts when I'm writing full Ruby programs? Not usually. I am not above shelling out to a command when I think it's easier though. For example, I've seen several complex and slow ways to generate a UUID in Ruby over the years, but I usually favor this cheat:

$ ruby -e 'uuid = `uuidgen`.strip; p uuid'
"6196D647-F6C4-4764-90A8-1775E22400B9"

It's been fast enough for any need I've ever had and I haven't had to run the code anywhere that didn't support this trick. (Windows probably wouldn't out of the box.)

This is the real win of Ruby's scripting heritage, in my opinion. It allows us to think about some problems in a different way. Instead of being forced to think, "I need to find a library for generating UUID's," Ruby makes it easy to substitute, "Doesn't Unix have a standard tool for this that I can talk to?" Would you try the same trick in a language like Java? Probably not, just because scripting support is a lot less robust there. That's why they tend to favor the libraries.

I vote we keep the I-can-always-fall-back-to-scripting attitude.

Life After Functional Programming

Does Ruby inherit from the functional programming languages? I would say that it definitely does. The functional languages are really where concepts like iterators originated. In fact, I believe Ruby's blocks in general were inspired by functional concepts.

Does that mean Ruby is a functional programming language? That question is a lot tougher, but I lean towards answering, "No." There are definitely folks that disagree with me on this point though.

Ruby methods aren't quite first-class functions. However, you can pretty much simulate that style of programming if you stick to using lambda(). That allows us to store "functions" in variables:

multiply = lambda { |l, r| l * r }
double   = lambda { |n| multiply[n, 2] }
triple   = lambda { |n| multiply[n, 3] }

(1..5).each_with_index do |n, i|
  puts unless i.zero?
  puts "      N: #{n}"
  puts "Doubled: #{double[n]}"
  puts "Tripled: #{triple[n]}"
end

Because the block passed to lambda() is a closure, we can later refer to those functions by name.

We can also return functions from functions:

build_multiplier = lambda { |multiplier|
  lambda { |n| multiplier * n }
}

double = build_multiplier[2]
triple = build_multiplier[3]

# ...

Of course, we can also pass functions to functions:

poor_mans_curry = lambda { |arg, f|
  lambda { |*rest| f[arg, *rest] }
}

double = poor_mans_curry[2, lambda { |l, r| l * r }]
triple = poor_mans_curry[3, lambda { |l, r| l * r }]

# ...

The example above also shows that anonymous functions are supported.

Ruby even has features to support this kind of work in some ways. I could have written the first example as:

multiply = lambda { |l, r| l * r }
double   = multiply.curry[2]
triple   = multiply.curry[3]

# ...

Since we've established that Ruby supports all of these functional features, why do I say it's not a functional language? Well, it's true that we can compose with functions in Ruby. That's pretty much the gateway to functional programming. But things don't end there.

Once you have those features, you start to do things in a certain way to maximize the benefits of using them. For example, it's ideal to use immutable data structures and treat variables more like constants. This allows you to guarantee that the same function called with the same arguments always returns the same results.

It's also very common to write recursive functions when programming functionally, so it's nice for the language to support that.

These are examples of areas where Ruby stops scoring so well. Of course, you can make immutable data structures and write recursive functions. However, there are downsides to using these tactics. Ruby's GC isn't really tuned for immutable data structures and you can definitely take some performance hits while the language struggles to clean up after you. Similarly, Ruby doesn't do tail-call optimization by default, so it's easy to exhaust the stack with a recursive function.

In my opinion, details like these make it clear that Ruby wasn't designed to favor this style of programming, even if it does make it possible. That's why I say that it's not really a functional language.

In Ruby, we could write a function to wrap another function and add some feature: say memoization. But the fact is that Ruby often has other ways to accomplish these tasks. For example, here's a dirt simple memoization technique for Ruby:

def fibonacci(nth, series = method(:fibonacci))
  if nth < 2
    nth
  else
    series[nth - 2] + series[nth - 1]
  end
end

# The easiest memoization in the world:
fibs = Hash.new { |series, nth| series[nth] = fibonacci(nth, series) }

p fibs[100]

The "function" above is originally recursive, though probably not as you normally see them written. I allow for the source of the series to be passed as an argument. That was just to make it trivial to swap it out later and that argument defaults to a reference of the method (Ruby's terminology) itself. This means it's really normal recursion (in disguise).

The main point here is that the default block of a Hash is actually just a memoization function already written for us in optimized C. We can just use a Hash to achieve that effect. It's clean and lightning quick.

I've seen multiple Rubyists over the years take detours into the functional languages, then come back and try to apply the concepts they have learned to Ruby itself. I did the same thing myself and wrote a lot about it. I never ended up finishing that series, because I realized that it wasn't really working. If I followed the functional code too closely, the resulting Ruby had issues. In other cases I really liked the code I came up with, but that's just because I translated the underlying concepts to how Ruby would better handle them (leaving functional programming behind).

The moral is that Ruby did inherit a lot from the functional programming languages, but you are probably already using the important bits. Work with blocks and iterators where they make sense and you will gain the most benefit from that style. Going any further than that may work for some cases, but the path is full of pitfalls.

It's Objects All the Way Down

Did I save the best for last? You bet!

Ruby clearly inherits from object oriented languages, like Smalltalk. That's a major focus of the design of the language.

The fact is that it really paid off too. Ruby has a gorgeous object model due to features like:

  • Almost everything is an object
  • Classes are objects too
  • Class definitions are just plain Ruby code being executed
  • Classes are open
  • Hooks like method_missing() and const_missing()
  • Per object behavior via singleton classes
  • Mix-ins

Sure, the system isn't perfect. We traded method overloading for dynamic typing (which is good and bad, in my opinion). We also don't really have keyword arguments. [Update: I am finally satisfied with Ruby's keyword arguments as of Ruby 2.1.] We do have techniques for getting around these deficiencies that generally seem to be good enough though.

What about multiple inheritance? Some say it's a feature. Others say it's a bug. It's definitely debatable, but Ruby seems dynamic enough to get by without it, in either case.

The fact is, whether by accidental or purposeful design, Ruby really has a terrific object model. The more I've learned about it over the years, the more I have come to appreciate it. I mean it's almost a zen moment when you finally realize that Ruby's method lookup is just a straight line and tools like classes, singleton classes, and mix-ins allow you to define methods at desired points on that line. It all fits together so well.

What's more, as I have been a Rubyist, I have studied some of the more classical material on object orientation. The more I learn from that world, the better my Ruby seems to get. It's almost exactly the opposite of trying to apply functional techniques to the language in that Ruby seems to welcome such ideas.

For example, look at a simple pattern like Pluggable Selector in Java, a mainstream object oriented language. Now observe the same pattern in Ruby:

class PluggableSelector
  def initialize(selector)
    @selector = selector
  end

  def run
    send("run_#{@selector}")
  end

  def run_foo
    "foo"
  end

  def run_bar
    "bar"
  end
end

result = PluggableSelector.new(:foo).run
puts "Ran #{result}"

Features like Symbol objects, interpolation, and send() just make this kind of work trivial. Ruby was made to do this.

Object thinking should always remain a big part of the Ruby we do.

What's Our Style?

I've argued that Ruby isn't just a scripting language, a functional language, or even just an object oriented language. It even has some other influences that I didn't visit here.

So, what kind of language is Ruby? What's the kind of programming that we do with it called?

I don't know that there is a name for it, but clearly it's a blend of many ideas. The trick is to find the right mix to make great code.

How do we define "great code?" Code's first priority is always to communicate with the reader. Start there.

Here's the mix I recommend for great Ruby:

  • 74.41% Object Oriented Programming: the classic techniques enhanced by Ruby's dynamic nature
  • 17.21% Functional Programming: mostly the bits Ruby has adopted, like blocks and iterators
  • 9.38% Scripting: for quick hacks and pragmatic shortcuts

You can clearly see by my exact figures and excellent math that this is a highly scientific calculation not to be argued with.

Further Reading

If you want to dig deeper into the various styles of programming, you might enjoy some of these resources:

  • The best guide to tactical object oriented programming I have ever read is easily Smalltalk Best Practice Patterns. It's worth learning a little Smalltalk just to read this book. It's that good.
  • You can often learn about using Ruby as a scripting language if you watch for articles that mention Ruby with a Unix slant. Here's a classic example and a newer one.
  • I haven't read it yet, but Seven Languages in Seven Weeks is on my reading list. It looks like a fun way to tour many different programming styles, including functional programming and prototype languages (which I didn't go into here).
Comments (0)
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader