Ruby Voodoo

Deep dives into random corners of my favorite programming language.

2

OCT
2008

Interpolation and Statements

I still cringe anytime I see code like:

"1 + 2 = " + (1 + 2).to_s  # => "1 + 2 = 3"

Some books even advocate the above, which is a real shame for Ruby.

I imagine most of you know that you can rewrite the above to use String interpolation:

"1 + 2 = #{1 + 2}"  # => "1 + 2 = 3"

Let's think about that simple code a little bit more than we usually do though. What's really going on here? Obviously #{ … } inserts the result of the embedded code in the String, but it's important to realize that it also calls to_s() on that result to make it fit in the String.

We can really make use of that knowledge if we try. Here's an example:

Name = Struct.new(:first, :last) do
  def full
    "#{first} #{last}".strip  # trick 1
  end
  alias_method :to_s, :full   # trick 2
end

Name.new("James").full                     # => "James"
Name.new(:James, :Gray).full               # => "James Gray"
"My name is #{Name.new('James', 'Gray')}." # => "My name is James Gray."

I've built a trivial data class for managing names here. In that, I've tried to make use of interpolation to the fullest.

First, I needed to be able to build a reasonable representation of a full name no matter what data I have. My first example shows how this might play out when I only have a first name. That means first() will return "James" and last() will return nil. There are several ways to deal with this, but I chose what I consider to be one of the easiest.

I want something like a String for first and last name. Interpolation pretty much enforces this for me, since it automatically calls to_s() on the interpolated values. It just so happens that nil.to_s is "" and following that up with a simple strip() will remove any excess space due to missing names.

You can also see from the second example that I don't have to be strictly using String objects for the names. Anything with a reasonable to_s() will do.

Taking that one step further, the third example shows that even Name itself can benefit from a reasonable to_s(). This allows me to drop the full object right into any old String. You can take this really far by just adding sensible to_s() definitions to all kinds of objects. You could have objects representing data changes dropping themselves right into audit logs, have game move classes automatically serialize themselves for transport over a network protocol, or anything else you can think of.

Hopefully I've made that point now. The implicit to_s() of String interpolation is nice. Now let's look at another aspect of Ruby.

Many languages go to great lengths to distinguish between concepts like a statement and an expression, what each of those does, and where they can appear. Thankfully, Ruby doesn't much care.

What does that mean for us? Well, just about everything evaluates to something. You can kind of think of it as everything having a return value. (It's not really a return value in the case of something like a conditional, but I think that description makes for a good visual of what we are talking about here.) Let me show some examples:

a = b = 2
a  # => 2
b  # => 2

num  = rand(20) - 10
type = if num >= 0 then :positive
       else             :negative
       end
[num, type]  # => [-3, :negative]

age   = rand(100)
group = case age
        when 0..18   then :child
        when 19..65  then :adult
        when 66..100 then :senior
        end
[age, group]  # => [44, :adult]

not_run = if false
            # ...
          end
not_run  # => nil

def do_nothing; end
not_run = do_nothing
not_run  # => nil

Hopefully something in there surprises you. Take special note of the last two which pretty much show that Ruby tends to just go with nil when there's nothing else for a construct to evaluate to. We can use that.

Now, let's throw those two concepts together. Interpolation calls to_s() on the results of any code and we now know that most any code will have a result. That means we can write code like this:

def pluralize(count, singular)
  "#{count} #{singular}#{'s' unless count == 1}"
end

3.times { |n| puts pluralize(n, "trick") }
# >> 0 tricks
# >> 1 trick
# >> 2 tricks

Obviously, this is a pretty dumb method compared to the similar functionality in ActiveSupport and other libraries, but check out that last interpolation. I dumped a full unless right in the String. If it triggers, it will evaluate to a simple 's', which may be an unusual use of a conditional but is exactly what we need here. When it doesn't trigger, we know Ruby will punt with nil and nil.to_s is nothing, so it won't affect our output at all.

Like anything, you can take this technique too far. If you find yourself embedding entire programs in a String it's time to turn in your keyboard. However, if you find yourself slinging to_s() calls like they are going out of style, there's probably some cleanup you could try.

In: Ruby Voodoo | Tags: Syntax | 7 Comments
Comments (7)
  1. Peter Haza
    Peter Haza October 7th, 2008 Reply Link

    I just love these posts! Ruby is still very new to me, but your posts just show how amazingly cool Ruby can be, and you show how you can take great advantage of this coolness.

    Thank you!

    1. Reply (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
    2. James Edward Gray II
      James Edward Gray II October 7th, 2008 Reply Link

      I'm glad to hear readers are enjoying them. Thanks for the high praise.

      1. Reply (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
  2. HansCz
    HansCz October 9th, 2008 Reply Link

    Hey James

    When my mother makes these great dishes that nobody else can do quite like her, I have taken to peering over her shoulder in the hopes I can learn to do it like her. Most of the time I'm busy guessing at ingredients and quantities, because she never weighs anything out. She does that by 'feel'. And most importantly: I watch her at work.

    I'm guessing your posts and screencasts are made of:

    • at least 5 cups of playful enthusiasm for Ruby
    • heaps of programming skills
    • 2 handfuls of intuition about pedagogy/teaching

    You have a certain tone that says to me: 'I am having fun doing this'.
    That makes it a joy to read, watch and learn.

    Thank you.

    P.S. Despite my hard work, I'm pretty sure you can still smash windows with my homemade meatballs ;-)

    1. Reply (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
    2. James Edward Gray II
      James Edward Gray II October 9th, 2008 Reply Link

      I am definitely missing the "heaps of programming skills" you credit me with, but you're right on that I love what I do. Can you believe people pay me to play with Ruby (my favorite toy)? It still surprises me.

      1. Reply (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
      2. HansCz
        HansCz October 10th, 2008 Reply Link

        I'm nothing if not an amateur, so I might not be the best judge of what 'heaps of programming skills' contain :-)

        They're no replacement for loving one's work, though.

        For my part, I couldn't tell you precisely what keeps me going. It's a wide variety of factors. One such is definitely the kick I get when the core of a work in progress works for the first time. In those moments I always feel like laughing loudly and exclaiming 'It's alive! muhahahahaaaa!' :)

        It's really insane the number of hours it can take me to get from no code to that moment.

        Sometimes I wish I was a street sweeper. At least they have shorter work cycles.

        1. Reply (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
  3. Geoffrey Grosenbach
    Geoffrey Grosenbach November 25th, 2008 Reply Link

    Guilty as charged. I hereby repent and won't use to_s in that context anymore.

    But how about this one:

    [first, last].join(' ')
    

    I think that looks nicer, especially when there are many elements. You can still call strip on the result.

    1. Reply (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
    2. James Edward Gray II
      James Edward Gray II November 25th, 2008 Reply Link

      That's a good point. It's worth noting that join() also includes a to_s() call for all members:

      >> [42, nil, Object.new].join(" ")
      => "42  #<Object:0x5303a4>"
      
      1. Reply (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
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