Advertisement

Ruby for Newbies: Operators and their Methods

by
This post is part of a series called Ruby for Newbies.
Ruby for Newbies: Iterators and Blocks
Ruby for Newbies: Working with Directories and Files

Ruby is a one of the most popular languages used on the web. We've started a new screencast series here on Nettuts+ that will introduce you to Ruby, as well as the great frameworks and tools that go along with Ruby development. In this lesson, we’ll be taking a deeper look at operators in Ruby, and why they are different from anything you’ve ever seen before.

Press the HD button for a clearer picture.

Subscribe to our YouTube page to watch all of the video tutorials!


Operators

You’ve familiar with operators.

1 + 2 # 3
 
person[:name] = "Joe"

Operators are things like the plus sign (one of the arithmetic operators), or the equal sign (the assignment operator). These things don’t look to much different from the ones you use in JavaScript, PHP, or any other language. But—like most of Ruby—there’s a lot more than meets the eye going on here.

Here’s the secret: operators in Ruby are really method calls. Try this:

1.+(2) # 3

Here, we’re calling the + operator on the object 1, passing in the object 2 as a parameter. We get back the object 3. We can do this with strings too:

name = "Joe"
 
name.+(" Smith") # "Joe Smith", but `name` is still "Joe"
 
name += " Smith" # name is now "Joe Smith"

As you can see, we can do string concatenation with the + method. As a bonus here, ruby defines the += operator based on the + operator (note: you can’t use += as a method).

As you might realize, this gives us incredible power. We can customize the meaning of adding, subtracting, and assigning objects in our custom classes. We saw how this works with properties on objects in our lesson on classes (we defined a property and property= method in the class, and got the expected syntax sugar for using them). What we’re looking at here is taking that a step further.


Building our own Operator Methods

Let’s try to create one of these methods ourselves. For this example, let’s create a refrigerator object, that we can add things to via the + operator and take things out of via the - operator.

Here’s the start of our class:

class Fridge
    def initialize (beverages=[], foods=[]) 
        @beverages = beverages
        @foods     = foods
    end
 
    def + (item)
 
    end
 
    def - (item)
 
    end
end

Our initialize function is pretty simple: we take two parameters (that fall back to empty arrays if nothing is given), and assign them to instance variables. Now, let’s build those two functions:

def + (item) 
    if item.is_a? Beverage
        @beverages.push item
    else
        @foods.push item
    end
end

This is pretty simple. Every object has an is_a? method that takes a single parameter: a class. If the object is an instance of that class, it will return true; otherwise, it will return false. So, this says that if the item we’re adding to the fridge is a Beverage, we’ll add it to the @beverages array. Otherwise, we’ll add it to the @food array.

That’s good; now, how about taking things out of the fridge? (Note: this method is different from the one shown in the video; this shows you that these operator method give us a great deal of flexibility; they are really just normal methods that you can do anything with. Also, I think this is a better version of the method; however, it’s more complex.)

def - (item)
    ret = @beverages.find do |beverage|
        beverage.name.downcase == item.downcase
    end
 
    return @beverages.delete ret unless ret.nil?
 
    ret = @foods.find do |food|
        food.name.downcase == item.downcase
    end
 
    @foods.delete ret
end

Here’s what’s going on when we use the minus operator. The parameter that it takes is a string, with the name of the item we’re looking for (By the way, we’ll create the Beverage and Food classes soon). We start by using the find method that arrays have. There are a few ways to use this method; we’re passing it a block; this block says that we’re trying to find the item in the array which has a name property that’s the same as the string we passed in; note that we’re converting both strings to lowercase, to be safe.

If there’s an item that matches in the array, that will be stored in ret; otherwise, ret will be nil. Next, we’ll return the result of @beverage.delete ret, which removes the item from the array and returns it. Notice we’re using a statement modifier at the end of that line: we do this unless ret is nil.

You might wonder why we’re using the keyword return here, since it’s not required in Ruby. If we didn’t use it here, the function wouldn’t return yet, since there’s more code to the function. Using return here allows us to return a value from a place the function wouldn’t normally return.

If we don’t return, that means the item wasn’t found in @beverages. Therefore, we’ll assume it’s in @foods. We’ll do the same thing to find the item in @foods and then return it.

Before testing this out, we’ll need our Food and Beverages classes:

class Beverage
    attr_accessor :name
 
    def initialize name
        @name = name
        @time = Time.now
    end
end
class Food
    attr_accessor :name
 
    def initialize name
        @name = name
        @time = Time.now
    end
end

Note that in the video, I didn’t make @name accessible from outside the object. Here, I’m doing that with attr_accessor :name, so that we can check the name of these object when they’re inside a fridge.

So, let’s test it out in irb; we’ll start by requiring the file that holds the code; then, give the classes a try; note that I’ve added line breaks to the output for easier reading.

> require './lesson_6'
=> true
 
> f = Fridge.new
=> #<Fridge:0x00000100a10378 @beverages=[], @foods=[]> 
 
> f + Beverage.new("water")
=> [#<Beverage:0x000001009fe8d0 @name="water", @time=2011-01-15 13:20:48 -0500>] 
 
> f + Food.new("bread")
=> [#<Food:0x000001009d3c98 @name="bread", @time=2011-01-15 13:20:59 -0500>] 
 
> f + Food.new("eggs")
=> [
    #<Food:0x000001009d3c98 @name="bread", @time=2011-01-15 13:20:59 -0500>, 
    #<Food:0x000001009746a8 @name="eggs", @time=2011-01-15 13:21:04 -0500>
   ] 
 
> f + Beverage.new("orange juice")
=> [
    #<Beverage:0x000001009fe8d0 @name="water", @time=2011-01-15 13:20:48 -0500>,
    #<Beverage:0x00000100907cd8 @name="orange juice", @time=2011-01-15 13:21:16 -0500>
   ]
 
> f
=> #<Fridge:0x00000100a10378 
        @beverages=[
            #<Beverage:0x000001009fe8d0 @name="water", @time=2011-01-15 13:20:48 -0500>,
            #<Beverage:0x00000100907cd8 @name="orange juice", @time=2011-01-15 13:21:16 -0500> ], 
        foods[
            #<Food:0x000001009d3c98 @name="bread", @time=2011-01-15 13:20:59 -0500>, 
            #<Food:0x000001009746a8 @name="eggs", @time=2011-01-15 13:21:04 -0500> ] 
> f - "bread"
=> #<Food:0x000001009d3c98 @name="bread", @time=2011-01-15 13:20:59 -0500> 
> f
=> #<Fridge:0x00000100a10378 
        @beverages=[
            #<Beverage:0x000001009fe8d0 @name="water", @time=2011-01-15 13:20:48 -0500>,
            #<Beverage:0x00000100907cd8 @name="orange juice", @time=2011-01-15 13:21:16 -0500>], 
        foods[#<Food:0x000001009746a8 @name="eggs", @time=2011-01-15 13:21:04 -0500>]

As we go along, you can see things being added to the @beverages and @foods arrays, and then subsequently removed.


Get and Set Operators

Now let’s write methods for the get and set operators used with hashes. You’ve seen this before:

person = {}
 
person[:name] = "Joe"

But, since these operators are methods, we can do it this way:

person.[]=(:age, 35) # to set
 
person.[](:name) # to get

That’s right; these are normal methods, with special sugar for your use.

Let’s give this a try; we’ll make a Club class. Our club with have members with different roles. However, we may want to have more than one member with a given role. So, our Club instance will keep track of members and their roles with a hash. If we try to assign a second member to a role, instead of overwriting the first one, we’ll add it.

class Club
    def initialize
        @members = {}
    end
 
    def [] (role)
        @members[role]
    end
 
    def []= (role, member)
 
    end
end

The get version is pretty simple; we just forward it to the @members array. But set is a little more complicated:

def []== (role, member)
    if @members[role].nil?
        @members[role] = member
    elsif @members[role].is_a? String
        @members[role] = [ @members[role], member ]
    else 
        @members[role].push member
    end
end

If that role has not been set, we’ll just set the value of that key to our member hash. If it has been set as a string, we want to convert that to an array, and put the original member and the new member in that array. Finally, if neither of those options are true, it’s already an array, and so we just push the member into the array. We can test this class this way:

c = Club.new
 
c[:chair] = "Joe"
 
c[:engineer] = "John"
 
c[:engineer] = "Sue"
 
c[:chair] # "Joe"
 
c[:engingeer] # [ "John", "Sue" ]

There you go!


Other Operators

These aren’t the only operators that we can do this with, of course. Here’s the whole list:

  • Arithmetic Operators: + - * \
  • Get and Set Operators: [] []=
  • Shovel Operator: <<
  • Comparison Operators: == < > <= >=
  • Case equality Operator: ===
  • Bit-wise Operator: | & ^

Thanks for Reading!

If you’ve got any questions about this lesson, or anything else we’ve discussed in Ruby, ask away in the comments!

Advertisement