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.
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!
Comments