Classes
You can think of classes as basic outlines of what an object should be made of and what it should be able to do. (For example, an object of class Car should have a color, make and model, and be able to move). A class in Ruby always starts with the keyword class followed by the name of the class. The name should always be in initial capitals. You terminate the class definition with the keyword end. For example:
class Person end
The above code creates an empty class Person. Now we need to define the variables and methods (also called data members) of the class. These data members describe the attributes of the objects (for example, a person can be 42 years old and male - these attributes are data members of the class Person). ---------------------------------------------------------------------------------------------------------------- Classes
There is a special initialize method available for classes which gets called when an object is created. It is defined inside a class just like any other class method:
class Person def initialize puts "Hi there" end end
The purpose of the initialize method is to initialize the class variables for a new object. (For example, when a Car object is created, the initialize method could set the number of tires to the value 4) The initialize method is known as the constructor in other object-oriented programming languages. ------------------------------------------------------------------------------------------------------------------ Objects
After the class and the initialize methods are defined, we can create objects of that class by using the new method. The method new is predefined in the Ruby library and does not need to be added to your class definition. Below is an example that creates two objects p1 and p2 of the class Person:
class Person def initialize puts "Hi there" end end
p1 = Person.new p2 = Person.new
# Output: # "Hi there" # "Hi there"
Note the syntax for calling new: the class name followed by a dot and followed by the method name. The code above outputs "Hi there" twice because we created two objects of the class, which each call the initialize method. Now, p1 and p2 are separate objects of the class Person.
Objects are also called instances of a class. The process of creating objects of a class is called instantiation. Note that in computing objects aren't always representative of physical items. For example, a programming object can represent a date, a time, and a bank account. --------------------------------------------------------------------------------------------------------------------- Instance Variables
An instance variable is one type of variable defined in a class. Each object of a class has a separate copy of the instance variables. Instance variables are preceded by the at sign (@) followed by the variable name (for example: @name) We can, for example, pass a parameter to the initialize method and assign it to an instance variable for a new object:
class Person def initialize(name) @name = name end end
In the code above, @name is an instance variable for the class Person. Now, we can create objects of that class and pass an argument to the new method:
p1 = Person.new("David") p2 = Person.new("Amy")
The object p1 now has an instance variable @name with the value "David" which relates only to the object p1. Similarly, @name for the object p2 is equal to "Amy". Each instance (object) of a class has its own unique instance variables that store values associated with that instance. You might wonder why we don't use local variables instead of instance variables. We need instance variables because their scope is the entire object, meaning that they are accessible inside all the methods for the object, opposed to local variables, which are accessible only within the scope they are declared, such as a single method. ----------------------------------------------------------------------------------------------------------------------- Instance Variables
A class can have multiple instance variables. For example: class Animal @age = 0 def initialize(name, age) @name = name @age = age end end
ob = Animal.new("Jacky", 3) ------------------------------------------------------------------------- Instance Methods
In the real world, objects behave in their own way. A car moves, a phone rings, and so on. The same applies to programming objects. Behavior is specific to the object's type and is defined by methods in the class. You can declare instance methods that are available to an object of the class. For example: class Dog def bark puts "Woof!" end end
We defined a method called bark that outputs text. Now we can instantiate an object and call the method using the dot syntax: d = Dog.new d.bark
# outputs "Woof"
As with any method, instance methods can include multiple parameters and return values. ---------------------------------------------------------------------------- Accessors
An instance method can also be created to access the instance variables from outside of the object. For example, if we want to access the @name instance variable for a Person object, we need a method that returns the value of that variable:
class Person def initialize(name) @name = name end def get_name @name end end
p = Person.new("David") puts p.get_name
# outputs "David" Try It Yourself
We created an instance method get_name that returns the value of the @name instance variable and then called it for our object p. Getter and setter methods are called accessors. The method that is used to retrieve the value of the variable is called a getter method (get_name in our example). The method that is used to modify the value of the variable is called a setter method. ------------------------------------------------------------------------------ Setter Methods
Getter methods are used to access the instance variable. If we want to change the value of the instance variables, we need setter methods. Ruby provides a special syntax for defining setter methods: the method name is followed by an equal sign (=). For example:
class Person def initialize(name) @name = name end def get_name @name end def set_name=(name) @name = name end end
p = Person.new("David") p.set_name = "Bob" puts p.get_name
# outputs "Bob"
In the code above, set_name is a setter method that sets the value of the @name instance variable to the value of its parameter name. Notice the special syntax used when calling the method: p.set_name = "Bob". Normally to call a method, you would use p.set_name=("Bob"), where the entire set_name= is the method name, and the string "Bob" is the argument being passed into the method. However, for setter methods, Ruby allows us to use a more natural assignment syntax: p.set_name = "Bob". When you see this code, just realize there's a method called set_name= working behind the scenes. ---------------------------------------------------------------------------------- Accessors
In Ruby it is a common practice to name the getter and setter methods using the same name as the instance variable they are accessing. The previous example can be rewritten as: class Person def initialize(name) @name = name end def name @name end def name=(name) @name = name end end
p = Person.new("David") p.name = "Bob" puts p.name
# outputs "Bob" ------------------------------------------------------------------------------------- Accessors
Imagine having a lot of instance variables and their setter and getter methods. The code would be really long. Ruby has a built-in way to automatically create these getter and setter methods using the attr_accessor method. The attr_accessor method takes a symbol of the instance variable name as an argument, which it uses to create getter and setter methods. We can do the following: class Person
attr_accessor :name
def initialize(name) @name = name end end
p = Person.new("David") p.name = "Bob" puts p.name
That one line replaced two accessor method definitions. Ruby also provides the attr_reader and attr_writer methods in case only a getter or setter method is needed for the instance variable. We can pass multiple symbols to the attr_accessor, attr_reader and attr_writer methods. For example: attr_accessor :name, :height, :weight ------------------------------------------------------------------------------------- Accessors
The accessor methods can also be called inside the class by using the self keyword. For example: class Person attr_accessor :name, :age def initialize(name, age) @name = name @age = age end def change(n, a) self.name = n self.age = a end def show_info puts "#{self.name} is #{self.age}" end end
p = Person.new("David", 28) p.change("Bob", 42) p.show_info
# outputs "Bob is 42"
In the code above, we define a method called change that changes the instance variables via their accessor methods. The show_info method outputs the values of the instance variables. self represents the current object and is used to call the instance methods and accessors of the object. One of the benefits of using self is for disambiguation. For example, if you have a variable and a method both called name, self.name would make it clear that you are referring to the method. ------------------------------------------------------------------------------------ Class Methods
Class methods are methods we can call directly on the class itself, without having to instantiate any objects. This can be useful when there is no logical need to create an object of the class, such as when a class is used to group similar methods and functionality (like mathematical operations). One example is a Math class that includes a square method for returning the square of its parameter. There is no logical need to create an object of the Math class just to call the method. This is where class methods come into play. Class methods are defined using the self keyword:
class Person def self.info puts "A Person" end end
Now, the method is a class method and can be called directly from the class, without the need of an object:
Person.info # outputs "A Person"
Remember, when used inside of instance methods, self is representing the current instance (object) of that class. When defining class methods, self is referring to the class itself, and not to an instance of the class. --------------------------------------------------------------------------------------- Class Variables
Class variables are accessible to every object of a class. A class variable belongs to the class, not the objects. You declare a class variable using two @ signs, for example @@name. We can, for example, keep count of all Person objects created using a class variable:
class Person @@count = 0 def initialize @@count += 1 end def self.get_count @@count end end
p1 = Person.new p2 = Person.new
puts Person.get_count # outputs 2
In the code above, @@count is a class variable. Since the initialize method is called for every object that is created, incrementing the @@count variable will keep track of the number of objects created. We also defined a class method called get_count to return the value of the class variable. In the code above, we have created two objects of the Person class so the value of the @@count variable is 2. Class variables are usually used when you need information about the class, not the individual objects. -------------------------------------------------------------------------------------------- Class Constants
A class can also contain constants. Remember, constant variables do not change their value and start with a capital letter. It is common to have uppercase names for constants, as in:
class Calc PI = 3.14 end
You can access constants using the class name, followed by two colon symbols (::) and the constant name, for example:
puts Calc::PI # outputs 3.14 --------------------------------------------------------------------------------------------- The to_s Method
The to_s method comes built-in with all classes. It gets called when you output the object. For example: class Person #some code end p = Person.new puts p # outputs "#<Person:0x0000000272e140>"
When we call puts p, Ruby automatically calls the to_s method for the object p, so puts p is the same as puts p.to_s. By default, the to_s method prints the object's class and an encoding of the object id. ----------------------------------------------------------------------------------------------- The to_s Method
We can define our own to_s method for a class and add custom implementation to it. For example, we can generate an informative, formatted output for our Person class: class Person def initialize(n, a) @name = n @age = a end def to_s "#{@name} is #{@age} years old." end end p = Person.new("David", 28) puts p # outputs: "David is 28 years old."
The to_s method also gets called when the object is used as a value in a string, like "#{p}" Defining the to_s method makes it easier and shorter to output the information of an object in the format needed, as opposed to defining a custom method and calling it from an object. When you define the to_s method you call puts on your object (puts obj), where with a custom method you have to explicitly call it from the object (puts obj.info). ----------------------------------------------------------------------------------------------- Inheritance
Inheritance is when a class receives, or inherits, attributes and behavior from another class. The class that is inheriting behavior is called the subclass (or derived class) and the class it inherits from is called the superclass (or base class). Imagine several classes, Cat, Dog, Rabbit and so on. Although they may differ in some ways (only Dog might have the method bark), they are likely to be similar in others (all having color and name). This similarity can be expressed by making them all inherit from a superclass Animal, which contains the shared functionality. The < symbol is used to inherit a class from another class. For example:
class Dog < Animal #some code end
In the code above, Dog is the subclass and Animal is the superclass. ------------------------------------------------------------------------------------------------- Inheritance
Now, let’s define the Animal and Dog classes: class Animal def initialize(name, color) @name = name @color = color end def speak puts "Hi" end end
class Dog < Animal end
Dog is a subclass of Animal so it inherits Animal's methods and attributes, making code like this possible: d = Dog.new("Bob", "brown") d.speak
# outputs "Hi"
Now Dog has all the methods and attributes of the Animal class, which is why we can instantiate the object and call the speak method. -------------------------------------------------------------------------------------------------- Inheritance
The subclass can also have its own methods and attributes. Let's define a Cat class and inherit it from the same Animal class:
class Animal def initialize(name, color) @name = name @color = color end def speak puts "Hi" end end
class Dog < Animal end
class Cat < Animal attr_accessor :age def speak puts "Meow" end end
c = Cat.new("Lucy", "white") c.age = 2 c.speak # outputs "Meow" Try It Yourself
In the code above, Cat inherits from Animal. It has an instance variable age and also defines its own speak method. This is called method overriding, because the speak method in Cat overrides, or replaces, the one in the Animal class. When we called the speak method for our Dog object, its superclass method was called because Dog did not override it. The Cat object called its own speak method, because it defined its own implementation. ------------------------------------------------------------------------------------------------ Inheritance
Inheritance is a great way to remove duplication in your code by writing the shared and common functionality in the superclass and then adding individual functionality in the subclasses. You can have multiple levels of inheritance, for example: class Animal end class Mammal < Animal end class Dog < Mammal end
Here, Dog inherits from Mammal, which inherits from Animal. This can be described as an "is a" relationship because a Dog is a Mammal, which is an Animal. This is an example of single inheritance with multiple levels of hierarchy. However, Ruby does not support multiple inheritance, meaning you cannot inherit a class simultaneously from multiple classes. (A class cannot have multiple superclasses) To achieve that behavior, Ruby supports mixins. We will learn about them in the next module. --------------------------------------------------------------------------------------------------- super
Ruby has a built-in method called super, which is used to call methods from the superclass. When you call super in a method of the subclass, the method of the same name gets called from the superclass. For example:
class Animal def speak puts "Hi" end end
class Cat < Animal def speak super puts "Meow" end end
super calls the speak method of the Animal class. Now, if we create an object of class Cat and call its speak method, we will get the following:
c = Cat.new c.speak # Outputs # Hi # Meow
The use of super allows us to remove duplicate code by using and extending the behavior of the superclass in our subclasses. ----------------------------------------------------------------------------------------------- super
super is more commonly used in the initialize method. For example, our superclass has a initialize method that takes one argument and initializes an instance variable:
class Animal def initialize(name) @name = name end end
Now, we need a subclass Cat that also has an @age instance variable, and we need to define its own initialize method. Instead of repeating ourselves, and setting the name instance variable in the Cat class, we can use its superclass with the super method as follows:
class Cat < Animal def initialize(name, age) super(name) @age = age end def to_s "#{@name} is #{@age} years old." end end
We passed one of the arguments to the super method, which calls the initialize method of the Animal class and sets the @name instance variable. Now we can instantiate an object and output its info:
c = Cat.new("Bob", 3) puts c
# outputs "Bob is 3 years old."
In the example we used super for a simple assignment. Imagine having a complex program with complex calculations and operations being carried out. That's where the real benefits of "not repeating yourself" come in, and calling the super where applicable is one way of achieving it. ----------------------------------------------------------------------------------------------------- Operator Overloading
Ruby allows us to overload operators so that we can perform operations such as adding two objects together. Let's say we have a class Shape, which has width and height properties. We want to be able to add together two Shape objects and, as a result, get a new object that has its width and height equal to the sum of the corresponding properties of the objects. All we need to do is define the corresponding operator as a method:
class Shape attr_accessor :h, :w def initialize(h, w) self.h = h self.w = w end
def +(other) Shape.new(self.h+other.h, self.w+other.w) end
end
a = Shape.new(7, 4) b = Shape.new(9, 18) c = a+b puts c.h # outputs 16 puts c.w # outputs 22
As you can see, the + method takes one argument, which is another Shape object, and returns a new Shape object with the corresponding values. You can override almost all operators in Ruby and have any custom logic defined in the corresponding method. ----------------------------------------------------------------------------------------------------- Access Modifiers
Until now, all the methods that we defined in our classes were publicly available, meaning that you could call those methods from outside of the class. There are certain situations when methods should only be visible to the class. For example, imagine a Banking class with methods to calculate values for internal transactions and operations. If these methods were available outside the class, the reliability of the data could be at risk. To control visibility, Ruby provides the public, private, and protected access modifiers. By default, all class methods (except initialize) are public, meaning that they are accessible from both inside and outside of the class. To make a method accessible only from inside the class, we can use the private access modifier:
class Person def initialize(age) @age = age end def show puts "#{@age} years = #{days_lived} days" end
private def days_lived @age * 365 end end
p = Person.new(42) p.show
In the code above, the method days_lived is private and is only accessible inside the class. We called it from the show method, which is public. If we try to call the days_lived method from an object (puts p.days_lived), we will get an error. When the reserved word private is used in a program, anything below it in the class is private (unless public or protected is placed after it to negate it). Access modifiers can be applied only to methods. Instance variable are always private. ------------------------------------------------------------------------------------------------------- Protected
An interesting thing to note about private Ruby methods is that they cannot be called with an explicit receiver, even if that receiver is itself. When we say "receiver", we mean the object that the method is being called from. Even if we try to call the private method with self we will get an error. This can be needed when, for example, overloading an operator to compare two objects using a private method. To demonstrate that, let's define a class Product with a private method id. If the ids of two products are equal, then they are considered equal:
class Product attr_accessor :name, :num def initialize(name, num) @name = name @num = num end def ==(other) self.id == other.id end private def id name.length*num end end
p1 = Product.new("PC", 5) p2 = Product.new("Laptop", 3) puts (p1 == p2) # outputs "...Error: private method 'id' called..."
This code generates an error, because we tried to call the private method id on self and the other object. To be able to do that without making the method public, Ruby has the protected access control. If we change the method from private to protected, the code will work:
class Product attr_accessor :name, :num def initialize(name, num) @name = name @num = num end def ==(other) self.id == other.id end protected def id name.length*num end end
p1 = Product.new("PC", 5) p2 = Product.new("Laptop", 3) puts (p1 == p2) # outputs false
So, protected methods are not accessible from outside code, just like private methods, but can be called for an object of the same class or subclasses. ---------------------------------------------------------------------------------------------------------- A Simple Game
Object Oriented Programming is very useful when it comes to creating complex applications, such as games. Let's create a simple fighting game, where two opponents will fight until one of them loses. We start by creating our Player class:
class Player attr_accessor :name, :health, :power def initialize(n, h, pow) @name = n @health = h @power = pow end def isAlive @health > 0 end def hit(opponent) opponent.health -= self.power end def to_s "#{name}: Health: #{health}, Power: #{power}" end end
The Player class has 3 instance variables, name, health and power, and 3 instance methods: isAlive shows whether the player is still alive. hit hits the opponent (decreases the opponent's health by the amount of player's power) to_s outputs the player information. We have also defined getter and setter accessors for the instance variables using the attr_accessor method. ------------------------------------------------------------------------------------------------------------- A Simple Game
With the Player class defined, we can now define a method to make two opponents fight: def fight(p1, p2) while p1.isAlive && p2.isAlive p1.hit(p2) p2.hit(p1) show_info(p1, p2) end
if p1.isAlive puts "#{p1.name} WON!" elsif p2.isAlive puts "#{p2.name} WON!" else puts "TIE!" end end
def show_info(*p) p.each { |x| puts x} end
The fight method uses a loop to make the players hit each other until the isAlive method returns false for one of them. After each iteration, the information of both players is output to the screen using the show_info method we defined. Once the loop is over (meaning one of the opponents has lost), we check and output the corresponding result. -------------------------------------------------------------------------------------------------------------- A Simple Game
All that is left is to create two Player objects and call the fight method. To make the game interesting, we can use random values for health and power of our players using the rand method, which returns a random value in the range of 0 to its argument. p1 = Player.new("Player 1", 1+rand(100), 1+rand(20)) p2 = Player.new("Player 2", 1+rand(100), 1+rand(20))
#show Player info show_info(p1, p2)
puts "LETS FIGHT!" fight(p1, p2)
We used 100 as the maximum value for health, and 20 as a maximum value for power. We add 1 to the rand method to avoid the value 0. Now, each time you run the program, two Players with random health and power will be created and will fight!
The final code:
class Player attr_accessor :name, :health, :power def initialize(n, h, pow) @name = n @health = h @power = pow end def isAlive @health > 0 end def hit(opponent) opponent.health -= self.power end def to_s "#{name}: Health: #{health}, Power: #{power}" end end
def fight(p1, p2) while p1.isAlive && p2.isAlive p1.hit(p2) p2.hit(p1) show_info(p1, p2) end
if p1.isAlive puts "#{p1.name} WON!" elsif p2.isAlive puts "#{p2.name} WON!" else puts "TIE!" end end
def show_info(*p) p.each { |x| puts x} end
#initialize Players puts "PLAYERS INFO" p1 = Player.new("Player 1", 1+rand(100), 1+rand(20)) p2 = Player.new("Player 2", 1+rand(100), 1+rand(20))
#show Player info show_info(p1, p2)
puts "LETS FIGHT!" fight(p1, p2)
This was just a simplified version. You can easily create different subclasses of players, add more properties, define weapons, get user input to make different decisions, and so on. ---------------------------------------------------------------------------------------------------------- Modules
Extracting common methods to a superclass, like we did in the previous section, is a great way to model concepts that are naturally hierarchical (a Cat is an Animal which is a Mammal). Another way of grouping methods together are modules. A module is a collection of methods that can be used in other classes (think about them as libraries providing common functionality). Modules are defined using the module keyword followed by the module name, which should start with a capital letter. For example:
module Flyable def fly puts "I'm flying!" end end
The code above declares a module called Flyable, which includes one method called "fly". A module can include as many methods as you want. As you can see the syntax is very similar to defining classes. But why use modules instead of classes? Tap Continue to discover! ------------------------------------------------------------------------------------------------------------- Modules
Now, imagine having a class hierarchy, with a superclass Vehicle and subclasses: Car, Jet, Plane. All have some shared functionality, which they inherit from the Vehicle class, but only Jet and Plane can fly. Instead of defining separate fly methods for both classes, we can define a module and include it (commonly referred to as "mix" it) in the classes.
module Flyable def fly puts "I'm flying!" end end
class Vehicle end
class Car < Vehicle end
class Jet < Vehicle include Flyable end
class Plane < Vehicle include Flyable end
Now Jet and Plane objects can fly, but objects of other classes won't be able to: ob = Jet.new ob.fly
A common naming convention for Ruby is to use the "able" suffix on whatever verb describes the behavior that the module is modeling, like Walkable, Swimmable, Runnable, etc. Not all modules are named in this manner, however, it is quite common. -------------------------------------------------------------------------------------------------------------- Modules
As you may recall, Ruby does not allow a class to inherit from multiple classes. However, a class can mix in multiple modules. Modules used this way are known as "mixins". So, basically, a class can have multiple mixins:
class Human include Walkable include Speakable include Runnable end
It is important to remember the following: 1. You can only inherit from one class. But you can mix in as many modules as you'd like.
2. If it's an "is-a" relationship, choose class inheritance. If it's a "has-a" relationship, choose modules. Example: a plane "is a" vehicle; a plane "has an" ability to fly.
3. You cannot instantiate modules (i.e., an object cannot be created from a module). Modules are used only for grouping common methods together. Classes are about objects; modules are about methods. Mixins give you a great, controlled way of adding functionality to classes. --------------------------------------------------------------------------------------------------------------- Mixins
The true power of mixins comes out when the code in the mixin starts to interact with code in the class that uses it. Ruby has a number of predefined mixins ready for you to use. Let's take the standard Ruby mixin Comparable as an example. The Comparable mixin can be used to add the comparison operators (<, <=, ==, >=, and >) to a class. For this to work, Comparable assumes that any class that uses it defines the operator <=>. So, as a class writer, you define the one method, <=>, include Comparable, and get six comparison functions as a result! Let's try this with our Cat class, by making the cats comparable based on their age. All we have to do is include the Comparable module and implement the comparison operator <=>.
class Cat attr_accessor :name, :age include Comparable def initialize(n, a) self.name = n self.age = a end def <=>(other) self.age <=> other.age end end
c1 = Cat.new("Bob", 3) c2 = Cat.new("Lucy", 7)
puts c1 < c2 # true Try It Yourself
The Comparable module allows you to easily add comparison operators based on any custom logic to your classes. ------------------------------------------------------------------------------------------------------------- Namespacing
We've already seen how modules can be used to mix-in common behavior into classes. Now we'll see two more uses for modules. The first case we'll discuss is using modules for namespacing. In this context, namespacing means organizing similar classes in a module. In other words, we'll use modules to group related classes. For example:
module Mammal class Dog def speak puts "Woof!" end end
class Cat def speak puts "Meow" end end end
We defined a module Mammal which groups together two classes, Dog and Cat. Now we can call classes in the module by appending the class name to the module name with two colons(::): a = Mammal::Dog.new b = Mammal::Cat.new
a.speak # "Woof" b.speak # "Meow" Try It Yourself
The advantages of namespacing classes: It becomes easy for us to recognize related classes in our code. It reduces the likelihood of our classes colliding with other similarly named classes in our code. We can have the same class names across different modules. ------------------------------------------------------------------------------------------------------------ Namespacing
Another use for modules is as containers for methods. This allows us to group together relevant methods and use them in our code. For example:
module MyMath PI = 3.14 def self.square(x) x*x end def self.negate(x) -x end def self.factorial(x) (1..x).inject(:*) || 1 end end
puts MyMath.factorial(8)
The code above defines a module called MyMath, which includes a constant called PI, and three class methods. The methods inside a module are defined as class methods (note the self keyword), and we call them using the dot syntax. You can call the methods using two colon syntax (::) as well (MyMath::factorial(8)), but the dot syntax is preferred. Again, the advantages of using modules to group methods includes preventing name collisions, meaning that we can have the same method names across multiple modules. --------------------------------------------------------------------------------------------------------- Structs
In some cases, there is no need for defining a fully structured class. Sometimes we need just a group of attributes bundled together (for example, defining points in a 2D space using x and y coordinates). We could, of course, create a separate class with all the instance variables and methods, but Ruby provides a shortcut to bundle a number of attributes together called a Struct.
Point = Struct.new(:x, :y)
In the code above, Point is a Struct, having two attribute accessors: x and y. Struct automatically creates its initialize method for the defined accessors, so now we can use Points just like a class, instantiating different objects from it.
origin = Point.new(0, 0) dest = Point.new(15, 42) puts dest.y # 42
Struct is a built-in Ruby class and makes it shorter to define simple classes, accessors, and their initialize methods. ------------------------------------------------------------------------------------------------------------ OStruct
OpenStruct (or OStruct) acts very similarly to Struct, except that it doesn't have a defined list of attributes. To use OStruct, we need to include the corresponding library using the require statement. require "ostruct"
person = OpenStruct.new person.name = "John" person.age = 42 person.salary = 250
puts person.name # John
As you can see, we can define any number of attributes on the fly. OStruct isn't as fast as Struct, but it is more flexible. ------------------------------------------------------------------------------------------------------------ OStruct
We can also initialize an OStruct using a hash. For example: require "ostruct"
person = OpenStruct.new(name:"John", age:42, salary:250)
puts person.name # John
Struct and OStruct provide a simple way to create data structures that have the behavior of a class. ------------------------------------------------------------------------------------------------------------- Standard Classes
Ruby provides a number of standard built-in classes that make our life a lot easier by providing useful methods for manipulating data. Some of the classes in previous lessons include Array, String and Struct. Another useful class is the Math class, which provides methods to perform mathematical operations. For example:
# square root puts Math.sqrt(9) # 3
# pi constant puts Math::PI
# trigonometry (sin, cos, tan) puts Math::cos(0) # 1 -------------------------------------------------------------------------------------------------------------- Time
The Time class represents dates and times in Ruby. # current time t = Time.now puts t
# year, month, day puts t.year puts t.month puts t.day
# custom date t = Time.new(1988, 6, 10)
# day of week: 0 is Sunday puts t.wday
# day of year puts t.yday ---------------------------------------------------------------------------------------------------------------- Procs
Ruby provides the ability to take a block of code, wrap it up in an object (called a proc), store it in a variable, and run the code in the block whenever you feel like (more than once, if you want). For example:
greet = Proc.new do |x| puts "Welcome #{x}" end
We created a Proc that takes a parameter and outputs a greeting, and assigned it to the greet variable. We run the code in the proc using the call method.
greet.call "David" greet.call "Amy"
# Outputs # "Welcome David" # "Welcome Amy"
The code between the do and end keywords can include any number of operations. -------------------------------------------------------------------------------------------------------------------- Procs
Procs are very similar to methods. They perform operations and can include parameters. What make Procs really powerful and unique is the ability to pass them into methods, because procs are actually objects. For example:
greet = Proc.new do |x| puts "Welcome #{x}" end
goodbye = Proc.new do |x| puts "Goodbye #{x}" end
def say(arr, proc) arr.each { |x| proc.call x} end
We have defined two procs and a method, that takes an array and a proc as its parameters. For each item in the array it calls the proc. Now, greet and goodbye are objects that contain the corresponding blocks of code. We can call the say method and pass our proc objects as parameters:
people = ["David", "Amy", "John"] say(people, greet) say(people, goodbye)
We can pass to our methods as many procs as we want. Using procs gives the added flexibility to be able to reuse code blocks in more than one place without having to type them out every time. Procs basically take blocks of code and embed them in an object, allowing them to be reused and passed around. -------------------------------------------------------------------------------------------------------------------- Procs
Let's create a program that counts the execution time of a block of code. We will define a method that takes a proc as its parameter and counts the time it takes to execute the proc.
def calc(proc) start = Time.now proc.call dur = Time.now - start end
Let's see it in action:
someProc = Proc.new do num = 0 1000000.times do num = num + 1 end end
puts calc(someProc)
With that tiny calc method, we can now easily calculate the execution time of any Ruby code! ---------------------------------------------------------------------------------------------------------------------- Lambdas
Lambdas are a variation of Procs. A lambda is actually an instance of the Proc class. To create a lambda in Ruby, you use the following syntax: talk = lambda {puts "Hi"}
Alternatively, the following syntax can be used: talk = ->() {puts "Hi"}
Just like with procs, you use the call method to execute the lambda:
talk.call
In other programming languages, a lambda is commonly referred to as an anonymous function. ----------------------------------------------------------------------------------------------------------------------- Lambdas
Lambdas as very similar to procs. However, there are some important differences. The first difference between procs and lambdas is how arguments are handled. Lambdas check the number of arguments, while procs do not. For example: talk = lambda { |x| puts "Hello #{x}" } talk_proc = Proc.new { |x| puts "Hello #{x}" }
talk.call "David" # outputs "Hello David"
talk_proc.call "Amy" # outputs "Hello Amy" Try It Yourself
As you can see, the lambda and proc worked the same way, when the number of arguments is correct. However, if we try to call them with incorrect number of arguments: talk_proc.call # ouputs Hello
talk.call # outputs "Error: wrong number of arguments (given 0, expected 1)" Try It Yourself
As you can see, when a lambda expects an argument, you need to pass those arguments or an Error will occur. However, in the case of the Proc, if the argument is not passed it automatically defaults to nil. A second difference between a lambda and a Proc is how the return statement is handled. When a lambda encounters a return statement it returns execution to the enclosing method. However, when a Proc encounters a return statement it jumps out of itself, as well as the enclosing method. -------------------------------------------------------------------------------------------------------------------- Creating Files
Until now we have been performing input and output tasks using the gets and puts methods. A more permanent form of input and output is files. Ruby allows us to easily create and work with files by using the built-in File class.
file = File.new("test.txt", "w+")
The code above creates a file named "test.txt" with the mode w+ for read and write access to the file. Note that the file will be created in the same directory as our program. Now we can use our file object to perform different operations. After using a file, it must be closed using the close method:
file.close
It is necessary to close open files so they no longer continue to occupy space in memory. -------------------------------------------------------------------------------------------------------------------- File Modes
Ruby supports the following file modes: r read-only, starts at beginning of file (default mode). r+ read-write, starts at beginning of file. w write-only, truncates existing file to zero length or creates a new file for writing. w+ read-write, truncates existing file to zero length overwriting existing data or creates a new file for reading and writing. a write-only, appends to end of file if file exists, otherwise creates a new file for writing. a+ read-write, appends or reads from end of file if file exists, otherwise creates a new file for reading and writing. When the open mode is read only, the mode cannot be changed to writable. Similarly, the open mode cannot be changed from write only to readable. --------------------------------------------------------------------------------------------------------------------- Opening Files
To open an existing file you use the File class open method:
file = File.open("filename", "w+")
As with the new method, the second argument of the open method specifies the mode. -------------------------------------------------------------------------------------------------------------------- Writing Files
The puts and write methods can be used for writing content to a file. The difference between the two is that puts adds a line break to the end of strings, while write does not. For example:
file = File.new("test.txt", "w+") file.puts("some text") file.close
Remember to close the file after performing the file operations. --------------------------------------------------------------------------------------------------------------------- Writing Files
If we want to write to an existing file, we can use a block of code to perform the write operation. With this code Ruby will automatically close the file. For example:
File.open("file.txt", "w+") { |file| file.puts("some text") }
This shorter way makes sure the file closes at the end of the block. ---------------------------------------------------------------------------------------------------------------------- Reading Files
To read the entire contents of a file the File.read method can be used. For example:
f = File.new("test.txt", "w+") f.puts("a line of text") f.puts("another line of text") f.close
puts File.read("test.txt")
# Outputs: # a line of text # another line of text ------------------------------------------------------------------------------------------------ Reading Files
We can also read the file contents line-by-line using the readlines method. For example: File.open("test.txt", "a+") { |file| file.puts("a line of text") file.puts("another line of text") }
File.readlines("test.txt").each { |line| puts " --- #{line}" }
The readlines method reads the entire file based on individual lines and returns those lines in an array. ------------------------------------------------------------------------------------------ Deleting Files
We can delete a file using the File.delete method. For example: File.delete("test.txt")
Be careful, as this command deletes the file permanently. ----------------------------------------------------------------------------------------------- Working with Files
When working with files, it is important to first check if the file exists in order to prevent an error. The file? method provides an easy way of checking if the file exists. For example:
File.open("test.txt") if File.file?("text.txt")
The code above tests whether a file exists before opening it. File.file? returns true if the file exists, and false if it does not. ------------------------------------------------------------------------------------------ File Info
Ruby provides some useful methods to get relevant information about a file. The size method returns the size of the file. The zero? method returns true if the named file exists and has a zero size (is empty). For example:
# create a file f = File.new("test.txt", "w+") f.puts("some file content")
puts f.size # 19
f.close
puts File.zero?("test.txt") # false --------------------------------------------------------------------------------------------- File Info
We can also check whether the file is writable, readable or executable: f = File.new("test.txt", "w+") f.puts("some content") f.close
puts File.readable?("test.txt") # true puts File.writable?("test.txt") # true puts File.executable?("test.txt") # false ------------------------------------------------------------------------------------------------
|