Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save uroborus/c61cd0d701815091190b to your computer and use it in GitHub Desktop.
Save uroborus/c61cd0d701815091190b to your computer and use it in GitHub Desktop.
# 0. Intro to objects
## Everything is object
We will begin our journey with objects.
In Ruby, just like in real life, our world is filled with objects. Everything is an object - integers, characters, text, arrays - everything.
To make things happen using Ruby, one always puts oneself in the place of an object and then has conversations with other objects, telling them to do stuff.
Roleplaying as an object in your program is an integral part of object-oriented programming. To know which object you are at the moment, one may use the keyword self.
Try it for yourself:
> self
output > main
## Talking to objects
One object interacts with another by using what are called methods. More specifically, one object "calls or invokes the methods" of another object.
In the example below, we call the method even? on the object that is the number 2 by placing a period (.) after the object, then adding in the name of the method we want to invoke.
> 2.even?
> true
Invoking a method on an object inevitably generates a response. This response is always another object. Calling the method next on the object 1 has it give us the next consecutive value, 2.
One may also chain method invocations by simply adding more periods and method names sequentially - each method in the chain is called on the result of the previous method. Go on and try it by invoking next twice on 1 to get 3.
> 1.next.next
> 3
# 0.1 More objects and methods
## Looking up methods
Ruby objects are happy to tell you what methods they provide. You simply call the methods method on them.
> methods
As you can see, you get a listing of all the methods on the number 1 that you could invoke. The names are prefixed with a colon (:) that you can safely ignore for now. If you find the results too muddled, you can easily sort them alphabetically. Try it for yourself - simply call the method sort on the result of methods:
> 1.methods.sort
> sort methdos
## Invoking methods with arguments
When talking to an object via its methods, it is possible to give it additional information so it can give you an appropriate response.
This additional information is called the "arguments to a method." The name "argument" makes sense if you stop to think about the fact that methods are the paths of communication between objects.
Here's an example of an argument to the method index, which finds the position of the argument in the array:
> ['rock', 'paper', 'scissors'].index('paper');
> 1
Here, index is the method and 'paper' the argument. If there is more than one argument, they can be passed to the method by simply separating them with commas.
Try using a method that takes two arguments - use the between? method to determine if the number 2 lies between the numbers 1 and 3.
> 2.between?(1, 3)
# 0.2 Syntactic Sugar for Special Methods
## special methdos
As an observant initiate, you've probably noticed that in the last lesson, Integer objects list mathematical operators like + and - among their methods. You probably also thought to yourself that invoking the + method like so - 1.+(2) - to add two numbers would be... clumsy.
> 4.+(3)
# 1.0 intro to string
## string construction
In doing lies the true path, so let us begin by doing. Type out "Ruby Monk" in the box below (remember to include the quotes).
Now click on 'Run', or better yet, hit Ctrl+Enter or Cmd+Enter.
> "Ruby Monk"
## Literal forms
String construction has what is known as a literal form - the interpreter treats anything surrounded with single quotes (') or double quotes(") as a string. In other words, both 'RubyMonk' and "RubyMonk" will create instances of strings.
It's your turn now; go ahead and create a string that contains the name of the current month. Like, 'April', or 'December'.
> 'June'
Action etches deeper into the memory than mere words. Recreate the string in the exercise above using double quotes ("). You'll see that the tests pass the same way as they did before.
> "June"
The single quoted and double quoted approaches have some differences, which we will look into later. For most purposes they are equivalent.
All Strings are instances of the Ruby String class which provides a number of methods to manipulate the string. Now that you have successfully mastered creating strings let's take a look at some of the most commonly used methods
## String Lenght
As a programmer, one of the common things you need to know about a String is its length. Instead of merely telling you how to do this, let me share a secret about the Ruby API with you: Try the obvious! The Ruby core libraries are very intuitive, and with a little practice you can effortlessly convert instructions in plain English to Ruby.
Test your intuition and try to change the code below to return the length of the string 'RubyMonk'.
> 'RubyMOnk'.lenght
# 1.1 String Basics
## String interpolation
It is essential to be able to replace placeholders within a string with values they represent. In the programming paradigm, this is called "string interpolation". In Ruby, string interpolation is extremely easy. Feel free to run the example below to see the sample in action.
> a = 1
>
> b = 4
>
> puts "The number #{a} is less than #{b}"
Do remember that placeholders aren't just variables. Any valid block of Ruby code you place inside #{} will be evaluated and inserted at that location. Isn't that very neat?
Now let us see you wield this tool. Complete the functionality of the method below which, given a String as the argument, inserts the length of that String into another String:
> def string_length_interpolater(incoming_string)
"The string you just gave me has a length of #{incoming_string.length}"
end
We've been using double quotes in all our string interpolation examples. A String literal created with single quotes does not support interpolation.
The essential difference between using single or double quotes is that double quotes allow for escape sequences while single quotes do not. What you saw above is one such example. “\n” is interpreted as a new line and appears as a new line when rendered to the user, whereas '\n' displays the actual escape sequence to the user.
Let us move on...
## Search in a String
Another common scenario is checking if a String contains any given character, word or sub-string. Considering your experience with the Ruby API and its intuitiveness, try and check whether the String given below includes 'Yoda'.
> "[Luke:] I can’t believe it. [Yoda:] That is why you fail.".include?("Yoda")
> Detects when the string includes 'Yoda' ✔
Did you manage to figure that out by yourself? Too many cooks spoil the broth while too many hints lead the hunter astray! Now check if the string below starts with 'Ruby'.
> "Ruby is a beautiful language".start_with?("Ruby")
> detects whether the string starts with 'Ruby' ✔
After that, checking whether the statement below ends with 'Ruby' should be easy.
> "I can't work with any other language but Ruby".end_with?("Ruby")
> detects whether the string ends with 'Ruby' ✔
Are you getting the hang of the Ruby API? The previous three methods all ended with a '?'. It is conventional in Ruby to have '?' at the end of the method if that method returns only boolean values. Though it is not mandated by the syntax, this practice is highly recommended as it increases the readability of code.
Sometimes we will be required to know the index of a particular character or a sub-string in a given String and conveniently Ruby provides a method on String that does exactly that. Try and find out the index of 'R' in the string below:
> "I am a Rubyist".index('R')
> finds the index of the letter 'R' ✔
## String case change
The last thing we will look into in this lesson is manipulating the case of strings. Ruby provides us with a convenient tool-set to take care of proper and consistent casing within strings. Let's start with converting a string in lower case to upper case.
// side note
puts = print
single quotes = no interpolation,. no escaping
interpolation = placeholding
// end side note
> puts 'i am in lowercase'.upcase
>
Similarly, one can convert a string to lower case as well. Ruby calls this method downcase. Convert the string below into lower case.
> 'This is Mixed CASE'.downcase
>
On a parting note, let us touch on an interesting method. When you encounter a mixed cased string, Ruby provides a way to swap the case of every character in it i.e. this method would convert "I Am MixEd" to "i aM mIXeD". Try to figure this method out and tell us if you ever come across a scenario where you find use for this. It might make a good story for a rainy night!
> "ThiS iS A vErY ComPlEx SenTeNcE".swapcase!()
> swaps the case of every letter in the statement ✔
# 1.2 advances strin operations
## Splitting strings
In this lesson, we will have a look at some vital methods that the String object provides for string manipulation.
Splitting strings on a particular word, escape character or white space to get an array of sub-strings, is an oft-used technique. The method the Ruby String API provides for this is String#split. Let us begin by splitting the string below on space ' ' to get a collection of words in the string.
> 'Fear is the path to the dark side'.split(' ')
> ["Fear", "is", "the", "path", "to", "the", "dark", "side"]
One can effectively manipulate strings by combining String#split and a Regular Expressions. We will take look at Regular expressions, their form and usage further down the lesson.
It is possible to similarly split strings on new lines, and parse large amounts of data that is in the form of CSV.
## Concatenation String
You can create a new string by adding two strings together in Ruby, just like most other languages.
> 'Ruby' + 'Monk'
> RubyMonk
Ruby often provides more than one way to do the same thing. The literal and expressive method for String concatenation is String#concat.
> "Ruby".concat("Monk")
> RubyMOnk
Let's try a more widely used alias. You can use '<<' just like '+', but in this case the String object 'Monk' will be appended to the object represented by 'Ruby' itself. In the first case of using '+', the original string is not modified, as a third string 'RubyMonk' is created. This can make a huge difference in your memory utilization, if you are doing really large scale string manipulations. Change the code above to use '<<' and see all the tests passing again.
## Raplacing a subtrisng
The Ruby String API provides strong support for searching and replacing within strings. We can search for sub-strings or use Regex. Let us first try our hands at something simple. This is how we would replace 'I' with 'We' in a given string:
> "I should look into your problem when I get time".sub('I','We')
> We should look into your problem when I get time
The method above only replaced the first occurrence of the term we were looking for. In order to replace all occurrences we can use a method called gsub which has a global scope.
> "I should look into your problem when I get time".gsub('I','We')
> We should look into your problem when We get time
If you haven't come across the term Regular Expression before, now is the time for introductions. Regular Expressions or RegExs are a concise and flexible means for "matching" particular characters, words, or patterns of characters. In Ruby you specify a RegEx by putting it between a pair of forward slashes (/). Now let's look at an example that replaces all the vowels with the number 1:
> 'RubyMonk'.gsub(/[aeiou]/,'1')
> R1byM1nk
Could you replace all the characters in capital case with number '0' in the following problem?
> 'RubyMonk Is Pretty Brilliant'.gsub(/[A-Z]/, '0')
> 0uby0onk 0s 0retty 0rilliant
## Find a substring using RegEx
We covered the art of finding the position of a substring in a previous lesson, but how do we handle those cases where we don't know exactly what we are looking for? That's where Regular Expressions come in handy. The String#match method converts a pattern to a Regexp (if it isn‘t already one), and then invokes its match method on the target String object. Here is how you find the characters from a String which are next to a whitespace:
> 'RubyMonk Is Pretty Brilliant'.match(/ ./)
> I
As you can see in the output, the method just returns the first match rather than all the matches. In order to find further matches, we can pass a second argument to the match method. When the second parameter is present, it specifies the position in the string to begin the search. Let's find the second character in the string 'RubyMonk Is Pretty Brilliant' preceded by a space, which should be 'P'.
> 'RubyMonk Is Pretty Brilliant'.match(/ ./, 10)
>
# 2.0 Boolean Expressions in Ruby
## beginner's buide to expressions in ruby
Ruby uses the == operator for comparing two objects.
Let us try a simple exercise: write an expression that checks whether the value of the variable name is "Bob":
> name == "Bob"
> true
The other usual operators like greater than (>), less than (<), greater than or equal to (>=) etc. are supported. The next exercise is to write an expression that validates whether age is less than or equal to 35.
> age <= 35 # change this expression
> true
## Combining Expressions using the && and || operators
You can use the keywords || (read as 'or'), && (read as 'and') to combine expressions. Try modifying the following expression to check whether age is greater than or equal to 23 and the name is either Bob or Jill.
> age >= 23 && (name == 'Bob' || name == 'Jill')
> true
Just like the order of operations in mathematical expressions (PEMDAS anybody?), Ruby also has a set of laws governing the precedence of its various operators. However it is not something you need to be concerned about for now. Just make sure to use parentheses generously so that the order of operation is unambiguous to Ruby as well as for someone reading your code.
## Negating expressions
Ruby lets you negate expressions using the ! operator (read as 'not'). For instance, ! (name == 'Jill') will return false if the name is Jill and true for any other name.
Now try writing a simple expression that accepts any name except Bob
> name != 'Bob'
> true
# if..else construct
## ruby conditional branching: the if statement
You can use Ruby's Boolean Expressions to specifiy conditions using the usual if..else construct. Let us take a look at a simple example:
> def check_sign(number)
> if number > 0
> "#{number} is positive"
> else
> "#{number} is negative"
> end
> end
When you run the above example, it will tell you that 0 is negative. But we know that zero is neither positive nor negative. How do we fix this program so that zero is treated separately?
Ruby gives you the elsif keyword that helps you check for multiple possibilities inside an if..else construct. Try using elsif to fix the code below so that when the number is zero, it just prints 0.
> def check_sign(number)
> if number > 0
> "#{number} is positive"
> elsif number < 0
> "#{number} is negative"
> else
> 0
> end
> end
Ruby also has an unless keyword that can be used in places where you want to check for a negative condition. unless x is equivalent to if !x. Here is an example:
> age = 10
> unless age >= 18
> puts "Sorry, you need to be at least eighteen to drive a car. Grow up fast!"
> end
## the ternary operator
n english, "ternary" is an adjective meaning "composed of three items." In a programming language, a ternary operator is simply short-hand for an if-then-else construct.
In Ruby, ? and : can be used to mean "then" and "else" respectively. Here's the first example on this page re-written using a ternary operator.
> def check_sign(number)
> number > 0 ? "#{number} is positive" : "#{number} is negative"
> end
## truthness of objects in ruby
The conditional statements if and unless can also use expressions that return an object that is not either true or false.
In such cases, the objects false and nil equates to false. Every other object like say 1, 0, "" are all evaluated to be true. Here is a demonstration:
> if 0
> puts "Hey, 0 is considered to be a truth in Ruby"
> end
summery: zero is true
# 2.2 loops in ruby
## loops in ruby
Loops are programming constructs that help you repeat an action an arbitrary number of times.
The methods Array#each, Array#select etc. are the most frequently used loops since the primary use of loops is to iterate over or transform a collection, something that we'll learn in the chapter on "Arrays in Ruby."
Here we will cover two basic looping constructs you can use to solve most other looping requirements that may come up.
### infinite loops
Infinite loops keep running till you explicitly ask them to stop. They are syntactically the simplest to write. Here goes one:
> loop do
> puts "this line will be executed for an infinite amount of time"
> end
The example above does not have a termination condition and hence will run till the process is stopped. A loop can be halted from within using the break command.
Now write an infinite loop where the monk will meditate till he achieves Nirvana. Use the break statement once Nirvana is reached.
> # you have to change the following statement (into multiple ones if needed)
> # so that the monk meditates till monk.nirvana? becomes true.
> loop do
> monk.meditate()
> if monk.nirvana?
> break
> end
> end
## Run a block of code N times
Say N is 5, let us imagine how it might look.
> 5.times do
> # do the stuff that needs to be done
> end
Well, we imagined it right. It is that simple!
Here is a task for you to test your newly learned looping skills. We have a bell outside our monastery that people can ring in times of need. Write a method that can ring the bell N times, where N is a parameter passed to the method.
> # add a loop inside this method to ring the bell 'n' times
> def ring(bell, n)
> n.times do
> bell.ring
> end
> end
# 3.0 intro to arrays
## empty arrays
It is easier than you think.
There is a [] typed into the code editor already. This is how you create an array.
> []
You can also do the same thing by Array.new.
> Array.new
## Building arrays
You can create an array with a set of values by simply placing them inside [] like this: [1, 2, 3]. Try this out by creating an array with the numbers 1 through 5, inclusive.
[ 1,2,3,4,5]
Arrays in Ruby allow you to store any kind of objects in any combination with no restrictions on type. Thus, the literal array [1, 'one', 2, 'two'] mixes Integers and Strings and is perfectly valid.
## looking up data in arrays
Looking up values within an array is easily done using an index. Like most languages, arrays in Ruby have indexes starting from 0. The example below demonstrates how to look up the third value in an array.
> [1, 2, 3, 4, 5][2]
Now it's your turn - extract the 5th value from the array below. Remember that the nth value in an array has an index of n-1.
> [1, 2, 3, 4, 5, 6, 7][4]
Array indexes can also start from the end of the array, rather than the beginning! In Ruby, this is achieved by using negative numbers. This is called reverse index lookup. In this case, the values of the index start at -1 and become smaller. The example below returns the first value in the array.
> [1, 2, 3, 4, 5][-5]
Go ahead on try it out - extract the last value from the array below.
> [1, 2, 3, 4, 5][-1]
## Growing arrays
In Ruby, the size of an array is not fixed. Also, any object of any type can be added to an array, not just numbers. How about appending the String "woot" to an array? Try using << - that's the 'append' function - to add it to the array below.
> [1, 2, 3, 4, 5] << "woot"
Unlike many other languages, you will always find multiple ways to perform the same action in Ruby. To append a new element to a given array, you can also use push method on an array. Add the string "woot" to given array by calling push.
> [1, 2, 3, 4, 5].push( "woot" )
# 3.1 basic array operations
## transforming arrays
Now on to more interesting things, but with a little tip from me first. Try running this:
> [1, 2, 3, 4, 5].map { |i| i + 1 }
You'll notice that the output, [2, 3, 4, 5, 6] is the result of applying the code inside the curly brace to every single element in the array. The result is an entirely new array containing the results. In Ruby, the method map is used to transform the contents of an array according to a specified set of rules defined inside the code block. Go on, you try it. Multiply every element in the array below by 3 to get [3, 6 .. 15].
> [1, 2, 3, 4, 5].map{ |i| i*3 }
## Filtering elements of an Array
Filtering elements in a collection according to a boolean expression is a very common operation in day-to-day programming. Ruby provides the rather handy select method to make this easy.
> [1,2,3,4,5,6].select {|number| number % 2 == 0}
The method select is the standard Ruby idiom for filtering elements. In the following code, try extracting the strings that are longer than five characters.
> names = ['rock', 'paper', 'scissors', 'lizard', 'spock']
> names.select{ |name| name.length > 5 }
## Deleting elements
One of the reasons Ruby is so popular with developers is the intuitiveness of the API. Most of the time you can guess the method name which will perform the task you have in your mind. Try guessing the method you need to use to delete the element '5' from the array given below:
> [1,3,5,4,6,7].delete(5)
I guess that was easy. What if you want to delete all the elem`ents less than 4 from the given array. The example below does just that:
> [1,2,3,4,5,6,7].delete_if{|i| i < 4 }
You'll notice that Ruby methods with multiple words are separated by underscores (_). This convention is called "snake_casing" because a_longer_method_looks_kind_of_like_a_snake.
Okay! Hands-on time. Delete all the even numbers from the array given below.
> [1,2,3,4,5,6,7,8,9].delete_if { |i| i % 2 == 0 }
Doing this in languages like C or Java would take you a lot of boiler plate code. The beauty of Ruby is in its concise but readable code.
# 3.2. iteration
## got for loops?
Ruby, like most languages, has the all-time favourite for loop. Interestingly, nobody uses it much - but let's leave the alternatives for the next exercise. Here's how you use a for loop in Ruby. Just run the code below to print all the values in the array.
> array = [1, 2, 3, 4, 5]
> for i in array
> puts i
> end
Ok, your turn now. Copy the values less than 4 in the array stored in the source variable into the array in the destination variable.
> def array_copy(source)
> destination = []
> for i in source
> if i < 4
> destination << i
> end
> end
> return destination
> end
## looping with each
Iteration is one of the most commonly cited examples of the usage of blocks in Ruby. The Array#each method accepts a block to which each element of the array is passed in turn. You will find that for loops are hardly ever used in Ruby, and Array#each and its siblings are the de-facto standard. We'll go into the relative merits of using each over for loops a little later once you've had some time to get familiar with it. Let's look at an example that prints all the values in an array.
> array = [1, 2, 3, 4, 5]
> array.each do |i|
> puts i
> end
Ok, now let's try the same thing we did with the for loop earlier - copy the values less than 4 in the array stored in the source variable to the array in the destination variable.
> def array_copy(source)
> destination = []
> source.each do |i|
> if i < 4
> destination << i
> end
> end
> return destination
> end
# 4.0 intro to ruby hashes
## creating a hash
A Hash is a collection of key-value pairs. You retrieve or create a new entry in a Hash by referring to its key. Hashes are also called 'associative arrays', 'dictionary', 'HashMap' etc. in other languages
A blank hash can be declared using two curly braces {}. Here is an example of a Hash in Ruby:
student_ages = {
"Jack" => 10,
"Jill" => 12,
"Bob" => 14
}
The names (Jack, Jill, Bob) are the 'keys', and 10, 12 and 14 are the corresponding values. Now try creating a simple hash with the following keys and values:
Ramen = 3
Dal Makhani = 4
Tea = 2
> restaurant_menu = {
> "Ramen" => 3,
> "Dal Makhani" => 4,
> "Tea" => 2
> }
## Fetch values from a Hash
You can retrieve values from a Hash object using [] operator. The key of the required value should be enclosed within these square brackets.
Now try finding the price of a Ramen from the restaurant_menu hash: write the name of the object, follow it with a square bracket, and place the key inside the brackets. In this case the key is a string so enclose the key in quotes.
> restaurant_menu["Ramen"]
## Modifying a Hash
Once you've created a Hash object, you would want to add new key-value pairs as well as modify existing values.
Here is how you would set the price of a "Ramen" in the restaurant_menu hash:
restaurant_menu["Ramen"] = 3
In fact, you can create a blank hash and add all the values later. Now why don't you try assigning the following values to an empty hash?
Dal Makhani: 4.5
Tea: 2
> restaurant_menu = {}
> restaurant_menu["Dal Makhani"] = 4.5
> restaurant_menu["Tea"] = 2
# 4.1 Hashes, in and out
## iterating over a hash
You can use the each method to iterate over all the elements in a Hash. However unlike Array#each, when you iterate over a Hash using each, it passes two values to the block: the key and the value of each element.
Let us see how we can use the each method to display the restaurant menu.
> restaurant_menu = { "Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2 }
> restaurant_menu.each do | item, price |
> puts "#{item}: $#{price}"
> end
The restaurant is doing well, but it is forced to raise prices due to increasing costs. Use the each method to increase the price of all the items in the restaurant_menu by 10%.
Remember: in the previous example we only displayed the keys and values of each item in the hash. But in this exercise, you have to modify the hash and increase the value of each item.
> restaurant_menu = { "Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2 }
> restaurant_menu.each do |item, price|
> restaurant_menu[item] = price * 1.1
> end
## extracting the keys and values from a Hash
Every Hash object has two methods: keys and values. The keys method returns an array of all the keys in the Hash. Similarly values returns an array of just the values.
Try getting an array of all the keys in the restaurant_menu hash:
> restaurant_menu = { "Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2 }
> restaurant_menu.keys()
## Newer, faster.
There are some little-known shortcuts for creating new hashes. They all provide a slightly different convenience. The latter two generate a hash directly from pre-existing key-value pairs. The first simply sets a default value for all elements in the hash. Let's take a look at that first.
> normal = Hash.new
> was_not_there = normal[:zig]
> puts "Wasn't there:"
> p was_not_there
> usually_brown = Hash.new("brown")
> pretending_to_be_there = usually_brown[:zig]
> puts "Pretending to be there:"
> p pretending_to_be_there
As you can see, where a "normal" hash always returns nil by default, specifying a default in the Hash constructor will always return your custom default for any failed lookups on that hash instance.
The other two shortcuts actually use the Hash class's convenience method: Hash::[]. They're fairly straight-forward. The first takes a flat list of parameters, arranged in pairs. The second takes just one parameter: an array containing arrays which are themselves key-value pairs. Whew! That's a mouthful. Let's try the first form:
> chuck_norris = Hash[:punch, 99, :kick, 98, :stops_bullets_with_hands, true]
> p chuck_norris
> {:punch=>99, :kick=>98, :stops_bullets_with_hands=>true}
Cool. And easy! You have to now use the second form the "new hash" shortcut in this exercise. Again, it takes an array of key-value pairs. These key-value pairs will just be 2-element arrays. I'll give you the key-value pairs to start with. Your objective is to build a Hash out of this array.
> def artax
> a = [:punch, 0]
> b = [:kick, 72]
> c = [:stops_bullets_with_hands, false]
> key_value_pairs = [a,b,c]
> Hash[key_value_pairs]
> end
> p artax
# 5.0 classes
## grouping objects
At this point, one may wonder if all objects are the same. Are the groupings we've already covered - numbers, strings, arrays - supported by the Ruby language?
Ruby does in fact allow one to define groups of objects, or, to use object oriented jargon, classes of objects.
One may look up the class of any object by simply calling the class method on it. Let's try it out with a few of the objects we're already familiar with.
> puts 1.class
> puts "".class
> puts [].class
As you can see, Ruby tells us what we already suspected - that 1 is a Fixnum (a special case of Integer), "" is a String and [] is an Array.
We can also turn this interrogation around. Here, we'll ask whether an object is_a? particular class.
> puts 1.is_a?(Integer)
> puts 1.is_a?(String)
## classes are people too
An important feature of classes in Ruby is that they too adhere to the "everything is an object philosophy." This may be of interest to those already familiar with C++ and similar languages where classes are special constructs that cannot be interacted with like normal objects.
So, in Ruby, classes themselves are simply objects that belongs to the class Class. Here's a simple example that demonstrates this fact:
> 1.class.class
## what do classes do?
The next logical question is, of course, do classes simply act as logical groupings, or do they actually do something?
In Ruby, like other class-based object oriented languages that you may already be familiar with, classes act as the factories that build objects. An object built by a certain class is called 'an instance of that class.' Typically, calling the new method on a class results in an instance being created.
Let's build an instance of the most basic kind of object by calling the method new on the object Object.
> object = Object.new
This object - or instance, to be more precise - can't do very much. However, instances of more powerful classes like Arrays and Strings can do a whole lot more as we'll see in other lessons. You will also learn to create your own classes which you can then use to build bigger and more powerful programs.
# 5.1. building you own class
## getting classy
Let us start by building a class of our very own. Of course, a class needs a job or a role that its instances fulfill, or we have no reason to create one. Let us start by creating a class that represents a simple geometric Rectangle.
As you may have noticed already, classes in Ruby have names beginning with a capital letter. This convention has other implications that we shall go into later - for now, all our classes shall have names that begin with a capital letter.
> class Rectangle
> end
This class is as yet incomplete. For a class to justify its existence, it needs to have two distinct features:
1. State - A class must have some kind of state that defines the attributes of its instances. In the case of a simple rectangle, this could simply be its length and breadth.
2. Behaviour - A class must also do something meaningful. This is achieved by the programmer adding methods to the class that interact with its state to give us meaningful results.
## state and behaviour
"What is your perimeter?" and "What is your area?" sound like an interesting questions to ask of a rectangle. This behaviour will be defined as methods on the class Rectangle. Starting from the behaviour and working backward is an excellent way to understand exactly what state we need, something we shall address in the next section. Since this is the first time you're creating a method, let me demonstrate this for you with the perimeter, and then you can try adding the area yourself.
> class Rectangle
> def perimeter
> end
> end
The keyword def is what we use to create a method called perimeter. You will observe that Ruby follows a two-space indentation convention and sections of code are typically closed using the keyword end. Note that both the class and the method are closed in this manner. Of course, this class still does nothing, because all we have is a stateless class with an empty method. Let us now try to fill up this empty method with the formula for the perimeter of a rectangle, which is 2 * (length + breadth).
> class Rectangle
> def perimeter
> 2 * (@length + @breadth)
> end
> end
You'll notice that the variable names length and breadth have an @ symbol placed in front of them. This is a convention which designates them as being a part of the state of the class, or to use jargon, they are the "instance variables of the class." This means that every instance of the class Rectangle will have its own unique copies of these variables and is in effect, a distinct rectangle.
If you actually run this code, though, you'll see that it fails. Try adding Rectangle.new.perimeter to the end of the previous example. See what happens? @length and @breadth don't have values yet because we have no way to initialize them. Lets remedy this now.
> class Rectangle
> def initialize(length, breadth)
> @length = length
> @breadth = breadth
> end
>
> def perimeter
> 2 * (@length + @breadth)
> end
> end
Observe the tests for this piece of code carefully - you'll notice that we can now create instances of Rectangle that correctly tell us their perimeter.
It's your turn now - extend this class to add a method that calculates area using the formula length * breadth.
> class Rectangle
> def initialize(length, breadth)
> @length = length
> @breadth = breadth
> end
>
> def perimeter
> 2 * (@length + @breadth)
> end
>
> def area
> @length * @breadth
> end
> end
# 6.1. being methodical
## whar are methods, really
We've already talked about how objects in Ruby are analogous to objects in the real world. We've also briefly covered how they interact with one another through a construct called a "method." Since everything useful in a program happens through objects collaborating using methods, understanding them well is very important.
When one object calls a method on another, it is simply telling it what to do. A method, then, is simply programming jargon for something one object can do for another.
In the example below, we ask the object that represents the integer 1 to give us the next integer in the sequence. Do keep in mind that in the context of the program, "we" simply means the current object.
> puts 1.next
So, to summarize, the data an object contains is what it is and its methods are what it can do. Implicit in this definition is the fact that the abilities of an object are limited to the methods it exposes.
## Objectifying methods
Methods aren't exempt from Ruby's "everything is an object" rule. This means that the methods exposed by any object are themselves objects, and yes, you can use them as such.
All objects in Ruby expose the eponymous method method that can be used to get hold of any of its methods as an object.
> puts 1.method("next")
Here, we ask the object that is the integer 1 to give us the instance of the method next.
The method object still maintains a relationship with the object to which it belongs so you can still call it using the eponymous call method and it responds like a normal invocation of that method.
See for yourself.
> next_method_object = 1.method("next")
> puts next_method_object.call
It's worth noting that in the normal course of things it is unlikely that you will need to - or should - look up a method object and use it as such.
## make it so
Let's write a method called reverse_sign that takes one object - an Integer - and changes a positive value to negative one, and vice-versa. We'll then dissect it, then move on to you practicing writing your own method.
> def reverse_sign(an_integer)
> return 0 - an_integer
> end
>
> puts reverse_sign(100)
> puts reverse_sign(-5)
There, that works perfectly, converting 100 to -100 and -5 to 5 by simply subtracting the number given from 0.
Let's dissect this example a little, and you can then practice by writing your own.
First, note that we use the def keyword to create a method called reverse_sign on the current object. Since Ruby doesn't allow us to use spaces in method names, we replace them with underscores instead. It's also recommended that, as a convention, method names be in lower case.
The reverse_sign method accepts one parameter or argument. This is simply jargon for the objects a method needs from the caller in order for it to do its job. In this case, it's an integer. A method can accept any number of parameters (or none).
The return keyword specifies the object to be returned to the caller when the method has done its work. If no return keyword is specified, the object created by the last line in the method is automatically treated as the return value. A method must always return exactly one object.
Finally, the method is closed using the end keyword. There - simple, right?
> def do_nothing
> end
>
> puts do_nothing.class
As you can see, even a method that does nothing at all and has no return produces an object - the nil. I'm printing out the class name because printing a nil returns an empty string, so you wouldn't see anything.
Be cautious when using return - calling return also exits the method at that point. No code in the method after the return statement is executed.
> def demonstrate_early_return
> return
> puts "You will never see this, because we never get here."
> end
>
> puts demonstrate_early_return.class
> NilClass
This last example demonstrates two things:
1. The return exits the method; the puts statement that comes right after is never run.
2. Calling return without specifying an object to return results in a nil, which is returned by default.
An excellent practice is to either avoid using return entirely or always use return in the last line of the method (which is effectively the same thing). Having a method exit in the middle is always surprising, so being consistent in this manner makes for code that's easier to understand.
## beam me up, Scotty
Your turn. Write a method called add_two that adds 2 to any number passed to it and returns the result. Yes, please feel free to experiment using next in addition to the more obvious route of simply adding the integer 2 to the incoming number.
> def add_two(argument)
> argument + 2
> end
A more subtle lesson we learn from this exercise - especially if you got this to work using both addition as well as next - is that:
1. As someone using a method, you don't usually care about how it works, just that it does.
2. As the author of the method, you're free to change its internal implementation so long as it continues producing the same result. Switch from addition to next and back again - the master is happy either way.
# 6.2. calling a methods
## cooperative objects
Thus far, we've only dealt with methods that only accept a single object as an argument or parameter. We'll expand this and talk about the ways in which methods can accept more than one parameter.
Let's take this slow and begin with two parameters.
> def add(a_number, another_number)
> a_number + another_number
> end
>
> puts add(1, 2)
So, adding a second parameter is really simple - just add the new parameter separated from the original by a comma.
> def add(a_number, another_number, yet_another_number)
> a_number + another_number + yet_another_number
> end
>
> puts add(1, 2, 3)
Parameters can have default values too. Let's say we usually add three numbers, but occasionally just add two. We can default the last parameter in the previous example to 0 if nothing is passed to it.
> def add(a_number, another_number, yet_another_number = 0)
> a_number + another_number + yet_another_number
> end
>
> puts add(1, 2)
Older versions of Ruby - 1.8.x and older - required you to set default values for parameters starting with the last parameter in list and moving backward toward the first. The current version of Ruby (1.9.x) no longer has this limitation, but it's worth mentioning since Ruby 1.8.7 is still in use.
Ok, your turn. I shall simply ask that you make the tests pass for the exercise below.
> def say_hello(name = 'Qui-Gon Jinn')
> "Hello, #{name}."
> end
## arraying your arguments
The list of parameters passed to an object is, in fact, available as a list. To do this, we use what is called the splat operator - which is just an asterisk (*).
The splat operator is used to handle methods which have a variable parameter list. Let's use it to create an add method that can handle any number of parameters.
We use the inject method to iterate over arguments, which is covered in the chapter on Collections. It isn't directly relevant to this lesson, but do look it up if it piques your interest.
> def add(*numbers)
> numbers.inject(0) { |sum, number| sum + number }
> end
>
> puts add(1)
> puts add(1, 2)
> puts add(1, 2, 3)
> puts add(1, 2, 3, 4)
The splat operator works both ways - you can use it to convert arrays to parameter lists as easily as we just converted a parameter list to an array.
I'll show you how we can splat an array of three numbers into a parameter list so that it works with one of the examples from earlier in this lesson that accepts exactly three parameters.
> def add(a_number, another_number, yet_another_number)
> a_number + another_number + yet_another_number
> end
>
> numbers_to_add = [1, 2, 3] # Without a splat, this is just one parameter
> puts add(*numbers_to_add) # Try removing the splat just to see what happens
If you know some of the parameters to your method, you can even mix parameter lists and splatting. Again, older versions of Ruby (1.8.x or older) required you to place splatted parameters at the end of the parameter list, but this is no longer necessary.
In the example below, I'll expand on an earlier example to allow for the sum to be printed as a part of a message. We know what the message is - but we don't know how many numbers we'll need to add.
> def add(*numbers)
> numbers.inject(0) { |sum, number| sum + number }
> end
>
> def add_with_message(message, *numbers)
> "#{message} : #{add(*numbers)}"
> end
>
> puts add_with_message("The Sum is", 1, 2, 3)
Why don't you try it on for size? Create a method called introduction that accepts a person's age, gender and any number of names, then returns a String that introduces that person by combining all of these values to create a message acceptable to the tests.
As always, your objective is to make all the tests pass. Pay careful attention to the feedback from the tests, because even a simple, misplaced comma in the message could cause them to fail.
> def introduction(age, gender, *names)
> "Meet #{names.join(" ")}, who's #{age} and #{gender}"
> end
## naming parameters
This last section on method invocation is easiest demonstrated, then explained. Pay careful attention to the invocation of the add method in the example below. Look at how neatly we are able to pass configuration options to the method; the user of the add method gets to decide if the absolute value should be returned and if rounding should happen.
> def add(a_number, another_number, options = {})
> sum = a_number + another_number
> sum = sum.abs if options[:absolute]
> sum = sum.round(options[:precision]) if options[:round]
> sum
> end
>
> puts add(1.0134, -5.568)
> puts add(1.0134, -5.568, absolute: true)
> puts add(1.0134, -5.568, absolute: true, round: true, precision: 2)
Ruby makes this possible by allowing the last parameter in the parameter list to skip using curly braces if it's a hash, making for a much prettier method invocation. That's why we default the options to {} - because if it isn't passed, it should be an empty Hash.
As a consequence, the first invocation in the example has two parameters, the second, three and the last, seemingly five. In reality, the second and third invocations both have three parameters - two numbers and a hash.
## A not-so-gentle workout
You are used to this by now. Write for me three methods - calculate, add and subtract. The tests should all pass. Take a look at the hint if you have trouble! And as a little extra hint: remember that you can use something.is_a?(Hash) or another_thing.is_a?(String) to check an object's type.
> def add(*numbers)
> numbers.inject(0) { |sum, number| sum + number }
> end
>
> def subtract(*numbers)
> numbers.inject(numbers[0] * 2) { |diff, number| diff - number }
> end
>
> def calculate(*args)
> options = args[-1].is_a?(Hash) ? args.pop : {}
> options[:add] = true if options.empty?
> return add(*args) if options[:add]
> return subtract(*args) if options[:subtract]
> end
# 7.1. lanbdas in ruby
## lanbdas
You may have heard of lambdas before. Perhaps you've used them in other languages. Despite the fancy name, a lambda is just a function... peculiarly... without a name. They're anonymous, little functional spies sneaking into the rest of your code. Lambdas in Ruby are also objects, just like everything else! The last expression of a lambda is its return value, just like regular functions. As boring and familiar as that all sounds, it gives us a lot of power.
As objects, lambdas have methods and can be assigned to variables. Let's try it!
> l = lambda { "Do or do not" }
> puts l.call
Cool. Notice that our anticipatorily apologetic string is the return value of the lambda which we see by printing it using puts. Now, while this is a lovely string, perhaps we'd like to return something more interesting. Lambdas take parameters by surrounding them with pipes.
> l = lambda do |string|
> if string == "try"
> return "There's no such thing"
> else
> return "Do or do not."
> end
> end
> puts l.call("try")
Even cooler. Note that we replaced the {} that wrapped the lambda with do..end. Both work equally well, but the convention followed in Ruby is to use {} for single line lambdas and do..end for lambdas that are longer than a single line.
Now go ahead and add a lambda to the following code which increments any number passed to it by 1.
> Increment = lambda { |number| number + 1 }
# 7.2 blocks in ruby
## lambdas vs blocks
A lambda is a piece of code that you can store in a variable, and is an object. The simplest explanation for a block is that it is a piece of code that can't be stored in a variable and isn't an object. It is, as a consequence, significantly faster than a lambda, but not as versatile and also one of the rare instances where Ruby's "everything is an object" rule is broken.
As with most things in programming, there is more to this story, but blocks are an advanced topic so lets keep things simple for now. If you're interested in studying blocks in more detail, take a look at our chapter on blocks in our book "Ruby Primer: Ascent" which addresses intermediate and advanced topics.
Let's look at an example of a block that does the same thing as the lambda you wrote in the previous exercise.
> def demonstrate_block(number)
> yield(number)
> end
>
> puts demonstrate_block(1) { |number| number + 1 }
Let's list the ways in which this example is different from the last exercise that we did when learning about lambdas.
* There's no lambda.
* There's something new called yield.
* There's a method that has the body of a lambda immediately after the parameter list, which is a little weird.
## skipping the details
It turns out that one of the most common uses for a lambda involves passing exactly one block to a method which in turn uses it to get some work done. You'll see this all over the place in Ruby - Array iteration is an excellent example.
Ruby optimizes for this use case by offering the yield keyword that can call a single lambda that has been implicitly passed to a method without using the parameter list.
If you'll review the example in the previous section, you'll notice the same pattern - one method, one block passed to it outside of the parameter list, and with the block called using yield.
Now for some practice. Using what you've learned from earlier examples and exercises, make the tests pass.
> def calculate(a, b)
> yield(a, b)
> end
// side note
you can yield several parameters
// end side note
# 8.1. getting modular
## mixing it up
Ruby modules allow you to create groups of methods that you can then include or mix into any number of classes. Modules only hold behaviour, unlike classes, which hold both behaviour and state.
Since a module cannot be instantiated, there is no way for its methods to be called directly. Instead, it should be included in another class, which makes its methods available for use in instances of that class. There is, of course, more to this story, but let's keep it simple for now.
In order to include a module into a class, we use the method include which takes one parameter - the name of a Module.
> module WarmUp
> def push_ups
> "Phew, I need a break!"
> end
> end
>
> class Gym
> include WarmUp
>
> def preacher_curls
> "I'm building my biceps."
> end
> end
>
> class Dojo
> include WarmUp
>
> def tai_kyo_kyu
> "Look at my stance!"
> end
> end
>
> puts Gym.new.push_ups
> puts Dojo.new.push_ups
In the example above, we have a Gym and a Dojo. They each have their own, distinct behaviour - preacher_curls and tai_kyo_kyu respectively. However, both require push_ups, so this behaviour has been separated into a module, which is then included into both classes.
## Some hierarchy and a little exercise
Just like all classes are instances of Ruby's Class, all modules in Ruby are instances of Module.
Interestingly, Module is the superclass of Class, so this means that all classes are also modules, and can be used as such. For detailed lessons on inheritance in Ruby, do take a look at our chapter on the subject in the "Ruby Primer: Ascent."
> module WarmUp
> end
>
> puts WarmUp.class # Module
> puts Class.superclass # Module
> puts Module.superclass # Object
Time for some practice! As always, make the tests pass. Note that the perimeter of both a square and a rectangle is calculated by summing up all of its sides.
> module Perimeter
> def perimeter
> self.sides.inject{|sum,x| sum + x }
> end
> end
>
> class Rectangle
> include Perimeter
>
> def initialize(length, breadth)
> @length = length
> @breadth = breadth
> end
>
> def sides
> [@length, @breadth, @length, @breadth]
> end
> end
>
> class Square
> include Perimeter
>
> def initialize(side)
> @side = side
> end
>
> def sides
> [@side, @side, @side, @side]
> end
> end
# modules as namespaces
## collision courses
Namespacing is a way of bundling logically related objects together. Modules serve as a convenient tool for this. This allows classes or modules with conflicting names to co-exist while avoiding collisions. Think of this as storing different files with the same names under separate directories in your filesystem.
Modules can also hold classes. In this example, we'll try and define an Array class under our Perimeter module from the last lesson. Notice how it does not affect Ruby's Array class at all.
> module Perimeter
> class Array
> def initialize
> @size = 400
> end
> end
> end
>
> our_array = Perimeter::Array.new
> ruby_array = Array.new
>
> p our_array.class
> p ruby_array.class
We have these two classes alongside each other. This is possible because we've namespaced our version of the Array class under the Perimeter module.
:: is a constant lookup operator that looks up the Array constant only in the Perimeter module.
What happens when we don't namespace our Array class?
> class Array
> def initialize
> @size = 400
> end
> end
>
> our_array = Array.new
>
> p our_array.class
Because Ruby has open classes, doing this simply extends the Array class globally throughout the program, which is dangerous and of course not our intended behaviour.
The examples above are a bit contrived for the sake of simplicity. The real problem that namespacing solves is when you're loading libraries. If your program bundles libraries written by different authors, it is often the case that there might be classes or modules defined by the same name.
We're assuming these two libraries gym and dojo have classes as shown in the comment above them.
> # class Push
> # def up
> # 40
> # end
> # end
> require "gym" # up returns 40
> gym_push = Push.new
> p gym_push.up
>
> # class Push
> # def up
> # 30
> # end
> # end
> require "dojo" # up returns 30
> dojo_push = Push.new
> p dojo_push.up
As the dojo library is loaded after gym, it has overriden gym's class definition of Push and therefore creates an instance of Push defined in dojo.
The way to solve this problem is to wrap these classes in appropriate namespaces using modules.
> # module Gym
> # class Push
> # def up
> # 40
> # end
> # end
> # end
> require "gym"
>
> # module Dojo
> # class Push
> # def up
> # 30
> # end
> # end
> # end
> require "dojo"
>
> dojo_push = Dojo::Push.new
> p dojo_push.up
>
> gym_push = Gym::Push.new
> p gym_push.up
When you're creating libraries with Ruby, it is a good practice to namespace your code under the name of your library or project.
## constant lookup
We used the constant lookup (::) operator in the last section to scope our class to the module. As the name suggests, you can scope any constant using this operator and not just classes.
> module Dojo
> A = 4
> module Kata
> B = 8
> module Roulette
> class ScopeIn
> def push
> 15
> end
> end
> end
> end
> end
>
> A = 16
> B = 23
> C = 42
>
> puts "A - #{A}"
> puts "Dojo::A - #{Dojo::A}"
>
> puts
>
> puts "B - #{B}"
> puts "Dojo::Kata::B - #{Dojo::Kata::B}"
>
> puts
>
> puts "C - #{C}"
> puts "Dojo::Kata::Roulette::ScopeIn.new.push - #{Dojo::Kata::Roulette::ScopeIn.new.push}"
There are a few things going on the example above.
* Constant A is scoped within Dojo and accessing it via :: works as expected.
* Same for constant B which is nested further inside Kata.
* Class ScopeIn is nested even deeper inside Roulette which has a method returning 15.
This tells us two important things. One, we can nest constant lookups as deep as we want. Second, we aren't restricted to just classes and modules.
You are given a library called RubyMonk. It contains a module Parser which defines a class CodeParser. Write another class TextParser in the same namespace that parses a string and returns an array of capitalized alphabets.
> module RubyMonk
> module Parser
> class TextParser
> def self.parse(argument)
> argument.upcase.split('')
> end
> end
> end
> end
If you prepend a constant with :: without a parent, the scoping happens on the topmost level. In this exercise, change push to return 10 as per A = 10 in the topmost level, outside the Kata module.
> module Kata
> A = 5
> module Dojo
> B = 9
> A = 7
>
> class ScopeIn
> def push
> ::A
> end
> end
> end
> end
A = 10
Using modules and namespacing is the standard way of organizing libraries with Ruby. It's a good practice to keep this in mind while writing one.
# 9.1 Streams
## I/O streams and the IO class
An input/output stream is a sequence of data bytes that are accessed sequentially or randomly. This may seem like an abstract, high-level concept -- it is! I/O streams are used to work with almost everything about your computer that you can touch, see, or hear:
* printing text to the screen
* receiving key-press input from the keyboard
* playing sound through speakers
* sending and receiving data over a network
* reading and writing files stored on disk
All of these listed operations are considered "side-effects" in Computer Science. The touch/see/hear metric doesn't seem to work for network traffic and disk activity but side-effects are not necessarily obvious; in these two cases, something in the world has physically changed even if you can't see it.
Comparatively, "pure" code is code without side-effects: code which simply performs calculations. Of course, a "pure" program isn't very useful if it can't even print its results to the screen! This is where I/O streams come in. Ruby's IO class allows you to initialize these streams.
> # open the file "new-fd" and create a file descriptor:
> fd = IO.sysopen("new-fd", "w")
>
> # create a new I/O stream using the file descriptor for "new-fd":
> p IO.new(fd)
fd, the first argument to IO.new, is a file descriptor. This is a Fixnum value we assign to an IO object. We're using a combination of the sysopen method with IO.new but we can also create IO objects using the BasicSocket and File classes that are subclasses of IO. We'll learn more about File in the next lesson.
I warned you it would feel a bit abstract! The notion of creating a "file descriptor" is inherited from UNIX, where everything is a file. Because of this, you could use the above technique to open a network socket and send a message to another computer. You wouldn't do that, of course -- you would probably use the BasicSocket (or perhaps TCPSocket) class we just mentioned.
Let's leave the abstract behind and find something a little more concrete. There are a bunch of I/O streams that Ruby initializes when the interpreter gets loaded. The list here may seem longer than if you run this locally. These examples are evaluated in the dense rubymonk environment consisting of Rails, Passenger, and all our other magic juice.
> io_streams = Array.new
> ObjectSpace.each_object(IO) { |x| io_streams << x }
>
> p io_streams
## standart ouptut, input, error
Ruby defines constants STDOUT, STDIN and STDERR that are IO objects pointing to your program's input, output and error streams that you can use through your terminal, without opening any new files. You can see these constants defined in the list of IO objects we printed out in the last example.
> p STDOUT.class
> p STDOUT.fileno
>
> p STDIN.class
> p STDIN.fileno
>
> p STDERR.class
> p STDERR.fileno
Whenever you call puts, the output is sent to the IO object that STDOUT points to. It is the same for gets, where the input is captured by the IO object for STDIN and the warn method which directs to STDERR.
There is more to this though. The Kernel module provides us with global variables $stdout, $stdin and $stderr as well, which point to the same IO objects that the constants STDOUT, STDIN and STDERR point to. We can see this by checking their object_id.
> p $stdin.object_id
> p STDIN.object_id
>
> puts
>
> p $stdout.object_id
> p STDOUT.object_id
>
> puts
>
> p $stderr.object_id
> p STDERR.object_id
As you can see, the object_ids are consistent between the global variables and constants. Whenever you call puts, you're actually calling Kernel.puts (methods in Kernel are accessible everywhere in Ruby), which in turn calls $stdout.puts.
So why all the indirection? The purpose of these global variables is temporary redirection: you can assign these global variables to another IO object and pick up an IO stream other than the one that it is linked to by default. This is sometimes necessary for logging errors or capturing keyboard input you normally wouldn't. Most of the time this won't be necessary... but hey, it's cool to know you can do it!
We can use the StringIO class to easily fake out the IO objects. Try to capture STDERR so that calls to warn are redirected to our custom StringIO object.
> capture = StringIO.new
> $stderr = capture
// a bit cheated
# 9.2 using the 'file' class
## opening and closing
Where we used IO.sysopen and IO.new to create a new IO object in the last lesson, we'll use the File class here. You'll notice it's much more straight-forward!
(Note that file.inspect will return a FakeFS::File -- this isn't a real File object because otherwise your "friend-list.txt" would conflict with other rubymonk users' "friends-list.txt". Don't worry -- it behaves just like a real File object. See for yourself! File#read is shown as an example.)
> mode = "r+"
> file = File.open("friend-list.txt", mode)
> puts file.inspect
> puts file.read
> file.close
mode is a string that specifies the way you would like your file to be opened. Here we're using r+, which opens the file in read-write mode, starting from the beginning. w opens it in write-only mode, truncating the existing file. You can take look at all the possible modes here.
It's worth noting that there are (many!) multiple ways of opening files in Ruby. File.open also takes an optional block which will auto-close the file you opened once you are done with it.
> what_am_i = File.open("clean-slate.txt", "w") do |file|
> file.puts "Call me Ishmael."
> end
>
> p what_am_i # comment this line
>
> File.open("clean-slate.txt", "r") {|file| puts file.read }
## reading and writing
Now we'll take a look at some methods to read from an I/O stream. In these examples, our I/O stream is a file, but remember: as described in the previous lesson, files behave just like any other I/O stream.
The File#read method accepts two optional arguments: length, the number of bytes upto which the stream will be read, and buffer, where you can provide a String buffer which will be filled with the file data. This buffer is sometimes useful for performance when iterating over a file, as it re-uses an already initialized string.
> file = File.open("master", "r+")
>
> p file.read
> file.rewind # try commenting out this line to see what happens!
> # can you guess why this happens?
>
> buffer = ""
> p file.read(23, buffer)
> p buffer
>
> file.close
Could you guess why we had to use File#rewind before the second call to File#read? It's okay if you couldn't or weren't sure. The reason is actually due to Ruby's internal handling of files. When reading from a File object, Ruby keeps track of your position. In doing so, you could read a file one line (or page, or arbitrary chunk) at a time without recalculating where you left off after the last read.
If this still isn't clear, try changing the first file.read to file.read(16) and then comment out the file.rewind line again. The position in the file should be obvious from the second read.
File#seek should solidify this idea even further. You can "seek" to a particular byte in the file to tell Ruby where you want to start reading from. If you want a particular set of bytes from the file, you can then pass the length parameter to File#read to select a number of bytes from your new starting point.
> p File.read("monk")
>
> File.open("monk") do |f|
> f.seek(20, IO::SEEK_SET)
> p f.read(10)
> end
> "Master loves you as he loves Jacob."
> "he loves J"
readlines returns an array of all the lines of the opened IO stream. You can, again, optionally limit the number of lines and/or insert a custom separator between each of these lines.
> lines = File.readlines("monk")
> p lines
> p lines[0]
> ["Master loves", "you as he", "loves Jacob."]
> "Master loves"
To write to an I/O stream, we can use IO#write (or, in our case, File#write) and pass in a string. It returns the number of bytes that were written. Try calling the method that writes "Bar" to a file named disguise.
> File.open("disguise", "w") do |f|
> puts f.write("Bar")
> end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment