Académique Documents
Professionnel Documents
Culture Documents
Please note that there is so much more to Ruby than just what is covered here. These are
merely the basics with which you will be able to write your own programs or edit code you’ve
seen somewhere without having to worry about breaking things.
It is suggested that you follow the order of the Table of Contents, as they are listed from
easy to hard. You will not understand Modules if you don’t understand Classes, for instance.
If you’d like to link to this document, you also have the ability of linking to
https://bit.ly/RubyBasics.
Cyan: This represents an additional note which may be confusing to beginners. It is mainly
meant for people who already know a bit about programming or have experience with other
programming languages. It’s also used to give extra information sometimes for those who
understand it and/or are interested in it. You will not miss anything important by skipping
over these.
Table of Contents
● 1: Variables and data types
● 2: Conditions
● 3: Methods
● 4: Arrays
● 5: Ranges and for-loops
● 6: Classes and objects
● 7: More classes and polymorphism
● 8: More variables and scopes
● 9: Hashes
● 10: Exception handling
● 11: Blocks and Procs
● 12: IO, Marshal, and Zlib
● 13: Modules
1: Variables and data types
Every proper programming language has variables. These variables are used throughout
your code or program to hold certain information so you can use it whenever you’d like.
Such a variable also has a name. You can look at it as a box: the name is like a label which
allows you to identify the different boxes (variables) you have, and the information/data the
variables store can be considered the content of those boxes.
Data types
The information a variable stores is also called the value of the variable. This value is of a
certain type: a number, text, a fraction, a list of things, or else. There are a few names to
remember here:
● Fixnum: This is an integer, referring to any number without any decimal numbers
(e.g. 3, -85, 9230)
● Float: This is a fraction, referring to any number with decimal numbers (e.g. 5.6,
-372.5234, 2.0)
● String: A series of characters which together represent a word, sentence, or else.
Here, we’ve created a new variable called my_variable and given it a value of 10. 10 is a
Fixnum (integer), so we would say that my_variable is a Fixnum or my_variable is of type
Fixnum.
Ruby is a dynamically typed language, which means you do not need to specify a data type
(such as Fixnum, Float, or String) when defining a variable. The value of the variable does
not always need to have the same type and may change, unlike statically typed languages
such as C#.
Strings
While Fixnums and Floats are very basic (5, 8.3), Strings have a bit more to them.
Let’s say we want to put Hello world! in a string. A string is created by surrounding the text
you want to put into the string with double quotes. This would get us "Hello world!". This
is now a valid string with the content of Hello world!.
There are a few exceptions to this basic rule though. If you want to use a double quote (“)
inside the string, such as in I like the word “wholesome”, you have to put a backslash (\)
before each double quote. In string-form, this would look like "I like the word
\"wholesome\"".
Aside from with double quotes, if you want to use an actual backslash inside the string, you
have to add a backslash before that backslash. For instance, Did you backup your C:\ drive?
would be "Did you backup your C:\\ drive?" as a string.
This process of adding backslashes to certain characters is called escaping that character.
When you escape the letter n, which would look like \n when used inside a string, is a new
line, for example.
If you wanted to create a variable of the String type, you would write down something akin to
this:
my_greeting = "Hello everyone!"
To print out the value of a variable, "Hello everyone!" in the example above, for now, you
should use print.
print my_greeting # This prints out "Hello everyone!" to the screen or
console.
As you can see, everything that comes after the # is grey. This is a comment, and not
actually part of the code. You can use it to leave notes inside your code to make it more
readable or understandable, which is what I will be doing a lot in this tutorial series.
Whenever I use #=>, that still is a comment, I just add the => part to indicate that it’s being
printed out.
To perform math, we take either a number or a variable and use one of the operators listed
above.
print 10 + 2 #=> 12
print 10 - 2 #=> 8
print 10 * 2 #=> 20
print 10 / 2 #=> 5
print 10 ** 2 #=> 100 (because 10 * 10 = 100)
This was with numbers, but we can also do this with plain variables:
num1 = 10
num2 = 2
=> 12
print num1 + num2 #
print num1 * num2 # => 20
Everything we’ve done so far doesn’t actually change anything though. It just performs the
math we want, and that’s that. If we want to actually change the value of a variable, we have
to reassign to that variable.
var = 10
print var #=> 10
var = 10 + 2
print var #=> 12
var = var + 2 # var was set to 12, so 12 + 2 = 14
print var #=> 14
So based on this example, to increment a variable by 2, you should use var = var + 2. While
this is fine, you can write it in a shorter way. Introducing:
● var = var + 2 is the same as var += 2
● var = var - 2 is the same as var -= 2
● var = var * 2 is the same as var *= 2
● var = var / 2 is the same as var /= 2
● var = var ** 2 is the same as var **= 2
a *= b
=> 20
print a #
a **= b # a is now 20 and b is still 2, so 20 ^ 2 = 20 * 20
print a # => 400 (20 * 20 = 40)
2: Conditions
We now know how we can define variables and give them different values, and we can even
do some basic math, but that’s it. In this tutorial, you’ll learn how to check variables to be
equal to something, greater than, and so forth. We do this using another set of operators: the
relational operators:
● == Equal to
● > Greater than
● < Less than
● >= Greater than OR Equal to
● <= Less than OR Equal to
● != NOT Equal to
We use this with a variable or number/string/int/etc on the left, and what you want to check
for on the right.
print 15 == 15 #=> true
print 15 > 15 #=> false
print 15 < 15 #=> false
print 15 >= 15 #=> true
print 15 <= 15 #=> true
print 15 != 15 #=> false
If-statements
We can use these conditions inside if-statements too. These basically check if the given
condition is true, and if so, runs the code inside it.
if 1 0 > 9
# This code will run!
end
Notable here is the end. Certain keywords, including if, must always be “closed”, so to
speak. We do this by writing down end after we write down the keyword. Where we do so
and what happens when we do so, depends on the keyword. I’ll touch upon them all briefly
when introducing a new keyword. If you forget the end, though, your code will raise the
infamous SyntaxError because there are either too many or too few ends.
Because we know 10 > 9 is always true (and can’t-not be true), it basically acts like so,
internally:
if t rue
# This code will always run!
end
if-statements test the condition for being true, as mentioned. This means it essentially does
(condition) == true. If (condition) is true, it will essentially evaluate as true == true,
and so it will always run the code inside.
Similarly, if you were to write if false, that will never run, because false == true is
false.
c = a > b
if c
# Snippet B
end
One thing new in this example is c = a > b. a > b is, as I said, a condition, and a
condition is either true or false. Since the condition a > b is actually false, c will be false.
Since you can do c = true and c = false, that means true and false are also data types.
true’s data type is TrueClass, and false’s data type is FalseClass.
Keyword Else
If you want to check if a condition is NOT true, you could either do the opposite (for instance,
the opposite of a > b is a <= b if you think logically, and the opposite of a == b is a != b)
or use the else keyword. See the example below:
age = 17
if age >= 1 8
# 18+
else
# 17 or younger
end
In this example, we’re checking if age is greater than or equal to 18. If that’s true, it runs the
code where # 18+ is. If it’s false, though, it will see if there is an else-condition - which there
is, in this case, and run the code inside there, which is # 17 or younger here.
New here is if !b. what ! basically does it invert the variable. If b is false, !b is true, !!b is
false, !!!b is true, etc. We use this to check for a variable being false - we hardly ever use b
== false, but instead do !b.
This also works for false and true themselves; !false #=> true, !true #=> false
I’ve just showed off all ! Not does, but there’s still && and ||.
Since we only want our code to run if A is true and B is false, we can look at the two
conditions we have, a and !b, and combine those two. Since we want BOTH to be true, we
use AND. This would get us a && !b. What we’ve done now is combine two conditions into
one condition which we can use in just one if-statement, like so:
a = true
b = false
if a && !b
# Run our code
end
This works exactly the same with numbers, floats, and strings. You’ll just need some other
operators:
age = 10
if age == 10
# Code A
end
if age == 11
# Code B
end
if age != 10 && age != 11
# Code C
end
In this example, we want to run Code A if age is 10, Code B if age is 11, and Code C
otherwise. One part of the condition for Code C is age != 10, which checks if age is not
equal to 10. It could still be 11 though, so we check for that too using age != 11. If that’s the
case, we can run Code C and we’d have our intended behaviour.
Keyword Elsif
The code above isn’t great though. We can compress it into fewer lines using the elsif
keyword. What we wrote above will check every if-statement, no matter if it has run any
previous code or conditions.
If age == 10 is true, for instance, we’re sure age == 11 and age != 10 are going to be
false, so we wouldn’t really need to even test those conditions anymore. What this means is
that we could skip those conditions, which we can achieve with else. This would get us
something like this:
age = 10
if age == 10
# Code A
else
if age == 11
# Code B
end
if age != 11
# Code C
end
end
The same applies to age == 11 and age != 11, so we can do the same there:
age = 10
if age == 10
# Code A
else
if age == 11
# Code B
else
# Code C
end
end
It first tries age == 10. If that’s true, it runs Code A. If it’s false, it tries age == 11. If that’s
true, it runs Code B. If it’s false, it’ll run the else-condition if present - which it is here.
Logical operator OR
Aside from NOT and AND, there’s also OR. This takes two conditions and as a whole, will be
true if either one of them is true.
Let’s say we want to know if age == 10 OR age == 11. We’d combine those two conditions
into one with ||, which gets us age == 1
0 || age == 11.
age = 11
if age == 10 || age == 11
# You are either 10 or 11 years old
end
Operator precedence
You should be careful if you use the OR condition in your code anywhere, as it may give you
undesired results. Just like in math, certain symbols/operators have precedence over others.
Let’s say we want to run Code A if gender is 0 and age is 10 or 11. We could write
something like this:
gender = 0
age = 10
if gender == 0 && age == 10 || age == 11
# Code A
end
...But this isn’t the behaviour we want. gender == 0 && age == 10 is one part of the
condition, and sure, that’s what we want. But the other part is age == 11, and if that’s true,
the condition as a whole is also true, regardless of the gender. And we don’t want that.
And that’d be that. While valid, sure, we can also use parentheses, just like in math. It
changes the precedence to give priority to whatever is in those parentheses and executes
that first.
gender = 0
age = 10
if gender == 0 && (age == 10 || age == 11)
# Code A
end
This first executes age == 10 || age == 11, and that would be true in this case. It takes
the result, true, and combines that with the condition as a whole: gender == 0 && true. So
now, the only extra condition it has to check is gender == 0, and if that’s true, the condition
as a whole is true, and therefore Code A would be run.
You can add parentheses wherever and whenever you want. (gender == 0 && ((age ==
10) || (age == 11))) is the exact same, for instance. Just make sure the amount of
opening and closing parentheses match up, or you’ll be greeted with a SyntaxError.
3: Methods
In short, all methods are, are shortcuts of longer bits of code. To get straight into creating
your own method, methods are created by writing the def keyword (as in “define”) followed
by the name you want to give your method, and then closed with end, just like with
if-statements.
def my_method
print "Printing from my_method!"
end
In this example, we’ve created a new method called my_method, which prints out
"Printing from my_method!". We then close that method with end and call/run/execute
the code inside that method by simply writing down the name of that method, my_method.
In various other languages, to call a method you have to follow the method name with (). In
Ruby, those parentheses are optional. They make it an explicit call; more on this near the
end of this tutorial.
Method scope
Let’s try to use a variable we created outside of the method inside the method.
a = 10
def work
print a
end
The work method is trying to access the a variable we created before defining the method.
But methods cannot access normal variables that were created outside the method.
ork
def w
a = 10
end
work
In this example, we defined and called a method called work, which creates a new variable
called a inside that method. That variable then exists inside that method, but when you try to
call it outside of that method, it raises an error. The variable only exists inside that method.
All this is because methods introduce a new scope. This means you can’t access
variables created inside that new scope, and when inside that new scope, you can’t access
variables created outside that scope.
ork
def w
# This is the scope of the 'work' method
end
def work
a = 2
print a
end
print a #=> 10
Whenever the work method is called, it creates a new a variable inside that new scope,
which only exists inside that scope. When the method ends, it goes back to the main scope
and the a variable of the work scope no longer exists. But since we’re back in the main
scope, we still have the a variable of that main scope, which is still 10 because it wasn’t
changed.
Method arguments
Although we can’t actually access variables created outside of a method, we can still give
information to that method by giving it an argument.
def work(my_argument)
print my_argument
end
When defining a new method, you can specify arguments between parentheses after the
name (separated with commas). When calling a method, you can give the method an
argument by putting a variable between parentheses after the call to the method, just like
work(1).
To further clarify the method above, this is essentially what happens internally:
def work
my_argument = 1
print my_argument
end
The method expects 1 argument, which it calls my_argument. The first argument passed
when we call the method is 1, so it essentially executes my_argument = 1 inside that
method. This means it creates a new variable and allows you to use it inside the method.
This also works for multiple arguments:
def work(a, b, c)
print a * b * c
end
a = 1, b = 2, c = 3
The amount of arguments the method expects and the amount of arguments given have to
match up though. The work method above expects three arguments (a, b, and c).
work(1, 2, 3) is valid because it provides three arguments.
work(1, 2) is invalid because it provides 2 out of 3 arguments (ArgumentError).
work(1, 2, 3, 4) is invalid because it provides 4 for 3 arguments (ArgumentError).
An exception to this, though, are methods that have default arguments. This means that a
certain argument will take on a certain value if no value is given.
def mult(a, b = 1)
print a * b
end
mult(6, 1
) #=> 6
mult(6) # => 6 (b defaults to 1)
mult(6, 2 ) #=> 12
However, mult(6, 2, 5) will still raise an ArgumentError, because it’s giving too many
arguments. So would mult, because it doesn’t give any arguments at all.
def var
print "Method!"
end
When we use var to refer to a method, we call the implicit. When we use var(), we call that
explicit - it can’t be anything else, it’s not ambiguous, and it makes perfectly clear that it’s
supposed to call a method. Therefore, if there isn’t a method called var and we use var(), it’ll
treat it as a method, even if it doesn’t exist.
var = "Variable!"
Whenever we call work with an argument, it returns the given argument. So work(6) is
basically the same as just 6. And as you can imagine, when you just write down 6 without
anything, no assignments, no calls, no nothing, it calls the method and it’s done. It doesn’t
do anything, just like work(6) # Nothing happens.
However, we can also make it do something else to better illustrate the use-cases for this.
def mult(a, b)
return a * b
end
var = 0
print var #=> 0
var = mult(5, 6)
print var #=> 30
return also immediately quits the method. Any code that would come after it is never called.
def work
return 1
print "This will never be printed!"
return -1
end
Yes, you can call methods in method arguments. And even methods in those. And methods
in those.
def mult(a, b = 2)
return a * b
end
var = mult(2, 2
)
print var #=> 4
So if you were to add anything below the line you’d want to be returned, it would return the
new last line instead. This is called implicit.
def mult(a, b = 2)
a * b
-1
end
var = mult(2, 2
)
print var #=> -1
But then again, you could just use the explicit return keyword and all would be good.
def mult(a, b = 2)
return a * b
-1 # This is never executed
end
var = mult(2, 2
)
print var #=> 4
You can also just write return without anything after it. In that case, it’ll just give back nil
and act as though it said return n il.
def seconds_lived(age)
if age < 0
return 0
elsif age > 122
return "Nobody has ever lived longer than 122 years!"
end
return 60 * 60 * 24 * 365 * age
end
This means that the size of the array is 3, but the last index is actually 2.
To retrieve an element of the array at a certain index, you would write down [index] after
the array.
my_array = [1,2,3]
print my_array[0] #=> 1
print my_array[1] #=> 2
print my_array[2] #=> 3
(You can have as many spaces as you want when creating an array, you can either have no spaces at all or you
can have tons)
You can also change an element just like you would assign to a normal variable. Where
my_array = 7 would change the whole array to 7, my_array[1] = 7 would only change
the second element (index 1) to 7.
my_array = [1,2,3]
print my_array #=> [1,2,3]
my_array[1] = 7
print my_array #=> [1,7,3]
my_array = 7
print my_array #=> 7
Different data types inside arrays
You can have all sorts of data types inside an array.
Unlike in statically typed languages like C and C#, an array does not accept just one data
type. You can have floats, integers, strings, array, hashes, custom objects, booleans,
whatever you want, all in one array.
a = ["hello", 3.14, ["this", "is", "an", "array", ["in", "an",
["array"]]], ["i", "bet", "you", "like", "that"]]
print a[0] #=> "hello"
print a[1] #=> 3.14
print a[2] #=> ["this", "is", "an", "array", ["in", "an", ["array"]]]
print a[2][0] #=> "this"
print a[2][1] #=> "is"
print a[2][2] #=> "an"
print a[2][3] #=> "array"
print a[2][4] #=> ["in", "an", ["array"]]
print a[2][4][0] #=> "in"
print a[2][4][1] #=> "an"
print a[2][4][2] #=> ["array"]
print a[2][4][2][0] #=> "array"
print a[3] #=> ["i", "bet", "you","like", "that"]
a[7] = 8
print a #=> [1,2,3,nil,nil,nil,nil,8]
The array is filled up with nil up to the index you’re assigning to. In this case, the highest
index was 2, and we assigned index 7. That means index 3, 4, 5 and 6 are empty, and those
are set to nil.
...into this:
gender = 1
life_expectancy = [79,80][gender] # gender is 1, so [79,80][1] #=> 1
print life_expectancy #=> 80
Methods on arrays
A data type can have a collection of methods. Just like we created methods in the main
scope in the tutorial on methods, you can also have methods on data types (such as Array).
These methods will be available to everything of that data type. Exactly how this works and
what this is will be covered in tutorials 6 and 7 (classes).
size: Returns the size of the array. (synonym: length; does the exact same.)
a = ["one","two","three","four"]
print a.size #=> 4
push(value): Adds value to the array.
a = ["one","two","three"]
print a #=> ["one","two","three"]
a.push("four")
print a #=> ["one","two","three","four"]
a.delete_at(1)
print a #=> ["one","three","four"]
insert(idx, value): Inserts value at the idx index, but doesn’t overwrite anything. The other
elements just move one over.
a = ["one","three","four"]
print a #=> ["one","three","four"]
a.insert(1, "
two")
print a #=> ["one","two","three","four"]
a.delete("two")
print a #=> ["one","three","one","three","one","three"]
include?(value): Returns true if the value exists in the array, false if not.
a = ["one","two","three"]
print a.include?("three") #=> true
print a.include?("four") #=> false
5: Ranges and for-loops
There’s yet another data type to introduce now, and that’s Range. A Range is, as the name
suggests, a range of numbers with a start and an end. If you want to make a range with the
number 1, 2, 3 and 4, you have two options:
r = 1..4
print r #=> 1..4 (1, 2, 3, and 4)
r = 1...5
print r #=> 1...5 (1, 2, 3, and 4)
You first type a number, 1 in this case (can also be negative). If you then write down two
dots, you’re including the number written down after the dots. If you write down three dots,
you’re excluding the number written down after the dots. So 1..4 starts at 1 and includes
up to 4. 1...5 starts at 1 and goes up to 5, but doesn’t include it.
You can’t have ranges go from big to small, though. You can’t use Floats either.
● 2..-2 is empty. Just like an empty array, [], this is empty. Nothing.
● 1.1..1.3 is also empty. It won’t work when you try to do anything with it (regardless
of using parentheses)
Important to note is that for-loops don’t introduce a new scope, which means the value of i
will be overwritten in the loop.
i = 9
print i #=> 9
for i in 1..3
print i #=> 1, 2, 3
end
print i #=> 3
At the last iteration of the for-loop, it essentially does i = 3. This isn’t reversed, so i
becomes 3 outside of the loop, too. This is because it’s the same scope. Look at it like this:
# Outside the loop
i = 9
print i #=> 9
i = 2
print i #=> 2
i = 3
print i #=> 3
The range 0...3 contains 0, 1, and 2 - the same indexes as in the array. This means that if
we iterate through this 0...3 range and use them as indexes in the array, we get every single
element in the array.
a = ["one","two","three"]
print a.size #=> 3
r = 0...a.size
print r #=> 0...3
for i in r # Rather than 'for i in 0...3', 'for i in var' works, too.
print a[i] #=> "one", then "two", then "three"
end
e = a[0]
print e #=> "one"
e = 7
print e #=> 7
print a[0] #=> "one"
We assign a new variable called e and set it to the first element of the a array, which is
“one”. Since that’s now a new variable, if we change its value, the value of the first element
in the array won’t change - only that new variable will.
If we take this concept and use it inside a for-loop, we get something a little bit like this:
array = ["one","two","three"]
for e in array # You can also do 'for e in ["one","two","three"]
print e #=> "one", then "two", then "three"
end
What we’re doing here is iterating through each element directly. First it does e = “one”, then
e = “two”, and then e = “three”, rather than the indexes.
You can change the value of e, too. It just won’t affect the array being iterated through.
array = [1, 2, 3]
for e in array
e *= 2 # the same as e = e * 2
print e #=> 2, then 4, then 6
end
print array #=> [1,2,3]
However, if you were to do this with indexes, you are actually changing the array:
array = [1, 2, 3]
for i in 0...array.size
array[i] *= 2 # We're directly modifying the array here
print array[i] #=> 2, then 4, then 6
end
print array #=> [2, 4, 6]
Calling methods on ranges
There’s just one method on ranges that’d be useful to know for now, and that’s include?(n).
Just like with arrays, it will return true if n is included in the range, and false if not.
You have to be careful when calling a method on a range though. If you were to type
something like this:
print 1..4.include?(2)
This is one of the only instances where parentheses with ranges really matter.
6: Classes and objects
Variables stores values of a certain type. The best way to refer to that is with object. The
number 10 is an object. If you assign 10 to a variable (e.g. my_var = 10), the variable will
have the object of 10. The type of the object is the same as the value’s type (data type), we’ll
just be referring to it differently.
A variable stores an object for when you need to use it multiple times, such as this:
gender = 0
Out of these, 0 and 79 can be reused because they’re associated with a variable (gender
and life_expectancy). The array [79,80] is still an object and a newly created array, but once
the line life_expectancy = [79,8 0][gender] is done, [79,80] is no longer accessible.
The object is disposed by the garbage collector (which is just something that deletes old
unreachable objects/code to lower RAM usage), because there’s no way to access [79,80],
as it isn’t a variable.
To programmatically determine the class of an object, you can use the .class method on the
object.
print "a".class #=> String
print 1.class #=> Fixnum
print 3.14.class #=> Float
print [1,2,3].class #=> Array
print (1..3).class #=> Range
print true.class #=> TrueClass
print false.class #=> FalseClass
print nil.class #=> NilClass
a = "hello"
print a.class #=> String
Technically, this means you could write code like this:
var = "17"
if var.class == Fixnum
print "var is a number!"
elsif var.class == String # This is true
print "var is a string!"
else
print "var is something else!"
end
Fixnum, String, Array, Range, FalseClass and such may refer to the class the object is an
instance of, but that too is actually an object; an object of type Class.
However, there is a method for this on all objects which you should use instead:
.is_a?(class). Just why you should use this and why there’s a difference will be covered in 7:
Classes and polymorphism.
var = [17]
if var.is_a?(Fixnum)
print "var is a number!"
elsif var.is_a?(String)
print "var is a string!"
elsif var.is_a?(Array) # This is true
print "var is an array!"
else
print "var is something else!"
end
var *= 2 is equal to var = var * 2, which, in this case (because var is 10), would be 10 * 2.
This multiplication is actually an instance method on the Fixnum class. Just like we saw
with arrays (array.size, array.delete_at, array.push), it calls a method on the object 10; the
* method. Another completely valid (and standard) way to perform 10 * 2 would be 10.*(2).
This is essentially the method below, but then for the Fixnum class:
def *(number2)
return some_way_to_perform_the_calculation(number2)
end
This kind of notation (10 * 2) isn’t possible to replicate; it, again, is just a built-in, hard-coded
shortcut in Ruby (and basically every other Object Oriented Programming language). It’s just
a thing you have to accept.
As mentioned, 10 is an object of the Fixnum class. The Fixnum class provides a layout or
structure for what 10 should do, and also provides the methods for it. These are called
instance methods.
If you try to multiply something that isn’t a number though (such as nil), you’ll get an error
similar to this:
var = nil
var *= 2 #=> NoMethodError: Undefined method '*' for nil:NilClass
var *= 3
print var #=> "aaaaaaaaa"
Unlike NilClass, String does have a * method. It just does something different than
multiplication. Array has one too:
var = [1]
var *= 3
print var #=> [1,1,1]
var = [1,2,3]
var *= 3
print var #=> [1,2,3,1,2,3,1,2,3]
This works perfectly fine; we’re performing “Hello!”.*(2) and setting var to the result of that,
and then print it out.
"Hello!" = 3.14
Both of these raise a SyntaxError when trying to start the script, stating “unexpected ‘=’”.
This is because “Hello!” is an object, and you cannot assign to an object. Objects are
temporary, unless stored somewhere (such as a variable or array). And only variables/arrays
can be assigned to with =.
Improper arguments
While 10 * 2 and 2 * 10 might do the same, that’s not the case when you try to multiply
different data types.
Fixnum#* expects a Fixnum or a Float.
String#* expects a Fixnum or a Float.
Array#* expects a Fixnum or a Float.
Now take “a” * 3 as an example. This calls String#* with a Fixnum, so this works fine. But if
you reversed it, 3 * “a”, we’re calling Fixnum#* with a String. This raises an error, because
Fixnum#* only accepts Fixnums and Floats - not Strings. The same applies to Arrays.
This isn’t something internal, but rather a design choice. You can write your own code in a
way that supports both ways, or you can write something that only supports one way - it’s up
to you to figure out what you want.
Two examples of normal methods below, illustrating different ways you can write it.
def mult(a, b)
if a.is_a?(Fixnum) || a.is_a?(Float)
if b.is_a?(Fixnum) || b.is_a?(Float)
return a * b
else
return "You can't multiply #{a.class} with #{b.class}!"
end
elsif a.is_a?(String) || a.is_a?(Array)
if b.is_a?(Fixnum) || b.is_a?(Float)
return a * b
else
return "You can't multiply #{a.class} with #{b.class}!"
end
else
return "You can't multiply #{a.class} with #{b.class}!"
end
end
As you can see, it really is just a choice you make here. Ruby chose not to support
3 * "hi", but it’s really up to you what you support and what you don’t.
7: More classes and polymorphism
Classes aren’t just stand-alone classes, but they’re actually kind of like a family tree in that
they have ancestors. A String, for example, has the Object class as ancestor (yes, Object is
also a class). To illustrate which ancestors a class has, we’ll write it down like so:
Kernel -> Object -> Comparable -> String
This is the ancestor line for the String class. The first class that comes before String,
Comparable, is the superclass of the String class.
Now let’s say we’re doing 12.odd? #=> false. This method returns whether or not the
number is odd (13.odd? would be true).
When you call a method on an object (an instance method), it will look at the highest class
there is, which is Fixnum in this case. If that Fixnum class contains an instance method
called odd?, it will call that method.
If that Fixnum class does not contain an instance method called odd?, it will look at the class
below it, which is Integer. If that Integer class contains an instance method called odd?, it’ll
call that. If not, it’ll check Precision, Numeric, Comparable, Object, and then Kernel. If it still
hasn’t found it after Kernel, it will raise a NoMethodError. This is the concept of
inheritance/polymorphism; a class will inherit all methods from its ancestors.
Comparable has an instance method called .is_a?, which Strings, Arrays, Floats, Integers
and what not all use. But Kernel also has an instance method called .is_a?, which is used by
classes that don’t have Comparable (such as NilClass). NilClass’s ancestors are:
Kernel -> Object -> NilClass
Speaking of .is_a?, if you give it a class that is a part of the class’s inheritance tree, it’ll also
return true.
14.is_a?(Numeric) #=> true
14.is_a?(Object) #=> true
14.is_a?(String) #=> false
Every single object in Ruby inherits both Kernel and Object, which means
var.is_a?(Object) is always going to return true no matter what (as long as var exists).
The same applies to var.is_a?(Kernel).
Applying knowledge on inheritance trees
This might sound useless, but it can actually come in handy.
Where normally, to determine if a number is a Float or a Fixnum, we’d use
var.is_a?(Float) || var.is_a?(Fixnum), their inheritance trees reveal this:
Fixnum:
Kernel -> Object -> Comparable -> Numeric -> Integer -> Fixnum
Float:
Kernel -> Object -> Comparable -> Numeric -> Float
What we can gather from this, is that var.is_a?(Numeric) will be true for both Floats and
Fixnums. You just have to know if any other classes use Numeric, but that isn’t the case, so
it’s safe to use. If we did Comparable, for instance, we’d also get String in there, which we
don’t want.
Now, if you type CustomClass in your code, you’re referring to this newly created class. If
you do this before the class is actually defined, it won’t exist yet, and using it will cause
errors. But now you can also use this in methods like something.is_a?(CustomClass), or
something.class == CustomClass.
We now have a class, but we don’t have any instances of it yet. To initialize an instance of
CustomClass, you write CustomClass.new. This returns an object, which is an instance of
CustomClass. You should store this in a variable somewhere so you can use it again later.
When you do [1,2,3].size #=> 3, you’re calling the instance method called size on an
instance of an array (hence it’s called instance method, duh). You can very easily add
similar instance methods to your class by simply defining a method like you normally would.
class CustomClass
def say_hello
print "Hello!"
end
end
To test this out, we create an instance of the class with CustomClass.new, and then call
.say_hello on it:
var = CustomClass.new
var.say_hello
This might be a bit confusing when you’re used to the special 1..6, “hello”, [1,2,3] formats
and the like to create an instance of a class. These are actually just very special ways to
create an instance of those classes - normally, for any class, you would use
<ClassName>.new. (And no, there’s no way to make one of these kind of formats yourself,
unfortunately. You’re stuck with .new).
Class methods
All methods we’ve seen so far were instance methods, but there’s also a thing called class
methods. As the name suggests, these are methods on the class rather than the instance.
Where an instance is a creation or version of the class, methods on the class are only
accessible on that class and will always be the same. They’re not bound to any instances.
An instance would be 10, “hello”, [1,2], 1..5, false, or else; these are versions or instances of
the classes, respectively Fixnum, S tring, Array, Range, and FalseClass, of which you can
have as many as you want. As there are no simple examples of class methods by default,
we’ll create our own first. You do everything the way you normally would when creating an
instance method in a class, but instead of def <MethodName>, you write
def self.<MethodName> (or def <ClassName>.<MethodName>, even).
class CustomClass
def self.say_hello
print "Hello!"
end
end
Another way to call class methods (which works only with class methods), is ::say_hello
rather than .say_hello on the class.
We would use this on CustomClass itself, and not on its instances:
CustomClass.say_hello #=> "Hello!"
var = CustomClass.new
var.say_hello #=> Undefined method 'say_hello' for #<CustomClass>
#<CustomClass> means it’s an i nstance of said class (it usually has a 0x0000 (hex) number after it - ignore this)
The constructor
Looking at this, you should think “But isn’t CustomClass.new also a class method?”, and
you’d be right. This might not seem very interesting, but Class::new (just like Class#method
refers to an instance method, Class::method refers to a class method in speech) is a very
special kind of class method. It creates an instance of itself and allocates the memory
needed, and then immediately after creation, it calls an instance method called initialize
if it exists - this is called the constructor of the class, which just means it’s called upon
creation. We can implement this and add code to it, if we want:
class C ustomClass
def i nitialize
p " This object was just created!"
end
def say_hello
print "Hello!"
end
end
We can even pass arguments via Class::new (the constructor). You just need to make sure
Class#initialize matches those arguments.
class CustomClass
def initialize(string)
print string
end
end
So in short: Class::new calls Class#initialize, and their arguments are matched up.
Class::new is internal and shouldn’t be changed, but Class#initialize can be overriden and is
called upon creation of the object. If you typo def initialize, it won’t be called. This can cause
you a severe headache if you don’t notice it if it doesn’t cause any errors, so make sure you
spell it right.
There’s another catch with the constructor though - its return value is disregarded. No matter
what you return in def initialize, it’s going to return the newly created object. You can’t
change that.
class CustomClass
def initialize
print "Hello!"
return 7
print "Goodbye!"
end
end
var = CustomClass.new
# Prints out "Hello!"
var.class #=> CustomClass
Normally, you’d expect var to be 7 and var.class to be Fixnum because the return value of
def initialize is 7, but this return value is disregarded. It does still cancel the rest of the
method, though, so “Goodbye!” is never printed.
class CustomClass
def say_hello
print "Hello!"
end
end
class CustomClass
end
var = CustomClass.new
var.say_hello #=> "Hello!"
It opens and closes the class, opens it again, adds a method, closes, and opens and closes
again. It doesn’t actually modify anything. The only time opening and closing directly may be
useful is if the class isn’t actually defined yet in the first place.
If you add a method that has the same name as a method that already exists, it will be
overwritten. All calls to that will then call the new method.
class CustomClass
def say_hello
print "Hello!"
end
end
class CustomClass
def say_hello
print "Hi!"
end
end
You can also add methods to already existing classes, or overwrite methods that already
exist (whether or not that’s a good idea is a different story, but it’s possible).
class String
def greet
print "Hello!"
end
end
Inheriting classes
By default, a new class inherits Object (which in turn inherits Kernel), which means the
inheritance tree for any new class is:
Kernel -> Object -> YourNewClass
To change which class a new class inherits from, you write < ClassName after the new class
name when you define a class:
class One
end
To overwrite this method, you just add the method to class Two and make it do something
else.
To open a class with inheritance back up later, you can either include the inheritance part of
leave it out - that doesn’t matter.
class One
end
class Two
end
class Two
end
class Two < One #=> Error: Superclass mismatch for class Two
end
class Two
def hi
p "Hello!"
super
end
end
Two.new.hi # Prints "Hello!", then calls One#hi which prints out "Hi!"
If none of the superclasses have a method by that name, it will raise an error.
class One
end
Here, age is what’s called a local variable. Any variable that doesn’t start with an uppercase
letter or a character like @, @
@ or $
, is always a local variable.
In a script, you have multiple scopes. A scope is essentially a range of code with its own
collection of methods, local variables, accessibility, et cetera. As we touched upon with
methods, a new method has a different scope than the “main” scope. This means that local
variables in the main scope aren’t the same as local variables in the method’s scope, even if
they have the exact same name.
Constants
While you can’t access local variables from different scopes, constants can help work
around that. You can have constants in the main scope, or a class’ scope. What
differentiates local variables from constants, is the uppercase letter all constants start with.
Variable would be a constant, whereas variable would be a local variable.
These constants are accessible from anywhere at any time, as seen in the example here.
Age = 17
def print_age
print Age
end
print_age #=> 17
This works because the constants can be accessed from different scopes too. The Age
constant is defined in the main scope, and is later accessed in the print_age method’s
scope. If you were to try this with a local variable, it would raise an error, stating the variable
doesn’t exist.
There’s one more difference aside from visibility/accessibility, which comes in when you try
to change the value of the constant during run-time. A constant is initialized once, and isn’t
meant to be changed after that.In most languages, this isn’t possible in the first place. In
Ruby, however, the value does in fact change, but you’ll get a warning that the constant has
already been defined before.
Constants and classes
Classes can also have constants within them. When you do, they’re treated kind of similar to
class methods and the like. Constants aren’t a part of an instance of the class, but they exist
on the class itself. To then reference the constant inside that class, you’ll first have to type
the name of the class, followed by ::, followed by the constant name.
ewClass
class N
Age = 17
end
print NewClass::Age #=> 17
The Age constant has been created not in the main scope this time, but in NewClass’s
scope. This means that you can’t access it directly (which would be print Age), but that you
have to go into the class’ scope first (NewClass::).
You can also have a class in a class in a class, et cetera et cetera. When you then want to
refer to a so-called subclass, you also type MainClass::SubClass to reference it. (A
subclass is completely independent of the class it’s in; it just has a different scope, which
means you always need to type MainClass::SubClass to use it (unless you’re in MainClass
itself, in which case just SubClass suffices, because you’re already in the MainClass’s
scope)).
class One
class Two
Three = 3
end
end
def print_age
print @age
end
end
var = NewClass.new
# var now carries around the @age variable internally.
var.print_age #=> 10
If you were to try this with local variables (without the @), it would perform age = 10 in def
initialize, but then that method ends and the variable is gone again. Then calling print_age
would try to access a non-existent variable, because there’s no local variable defined in
print_age called age.
This is solved by using instance variables. These variables exist as long as the object they’re
in exists. We can’t directly access this variable from outside of the class though, nor on the
object itself (var). You have to be in the class to manipulate instance variables. For this you
need to create helper methods, methods that allow you to read and write to the variable
from the outside. This is pretty basic:
class NewClass
def initialize
@age = 10
end
def get_age
return @age
end
def set_age(new_value)
@age = new_value
end
end
var = NewClass.new
print var.get_age #=> 10
var.set_age(17)
print var.get_age #=> 17
var.set_age(10)
print var.get_age #=> 10
But that’s convoluted and annoying to do for every single instance variable (if it’s a big class,
you can easily have dozens of instance variables). That’s why we can create getter and
setter methods. Getter methods are basically just methods with the same name as the
instance variable, which return the value of said instance variable (exactly the same as
get_age but with a different name). Setters are a bit different though. Rather than
set_age(10), setter methods allow you to use assignment with = to call the method (yes,
assignment is also a method!)
First to demonstrate the getter method:
class NewClass
def initialize
@age = 10
end
# Getter
def age
return @age
end
end
p NewClass.new.age #=> 10
# Getter
def age
return @age
end
# Setter
def age=(new_value)
@age = new_value
end
end
var = NewClass.new
p var.age #=> 10
var.age = 17
p var.age #=> 17
var.age=(10) # Identical to normal assignment
p var.age #=> 10
# You can even perform mathematical functions with it:
var.age **= 2
p var.age #=> 100
So when you create a method with an equal sign = at the end of the method name, that
method will be treated as an assignment method, with =.
There’s really no need for getter and setter methods, unless you want the instance variable
to BE public and to be used outside of the class itself.
These getter and setter methods can be very useful, but it’s a lot to write if you have a lot of
instance variables. That’s why there are shortcuts for these getters and setters:
● attr_reader: This is the getter method. Equal to def age in the previous examples.
● attr_writer: This is the setter method. Equal to def age=(v) in the previous
examples.
● attr_accessor: This is both the getter and the setter method. Equal to def a ge and
def age=(v) in the previous examples.
attr also means Attribute. Attribute reader (getter), Attribute writer (setter), Attribute accessor
(getter + setter).
To use these shortcuts, you choose the keyword followed by a colon :, followed by the name
of the instance variable. When used, it will basically turn into the proper method(s) in the
order you wrote them. This means that you can overwrite the functionality later, if you want,
just by typing out the method and implementing your desired functionality (just like a normal
method).
class NewClass
# They're usually at the top, along with constants
attr_accessor :age
def initialize
@age = 10
end
end
var = NewClass.new
print var.age #=> 10
var.age = 17
print var.age #=> 17
Class variables
Just like you have instance variables that are linked to instances of the class, class variables
exist only on the class itself, and are thus always the same no matter how you access it
(different instances of the same class can have different values for their instance variables,
this isn’t the case with class variables). All variables that start with @@ are class variables.
class NewClass
@@my_cvar = 0
end
We have no way to access this variable outside of the class currently, so we have to create
some more getter and setter methods. Since we’re dealing with class variables that are
shared across all instances of the class, we should make them class methods:
class NewClass
@@my_cvar = 0
def self.my_cvar
return @@my_cvar
end
def self.my_cvar=(v)
@@my_cvar = v
end
end
=> 0
print NewClass.my_cvar #
NewClass.my_cvar = 1
print NewClass.my_cvar # => 1
You might not realize why this is a class variable and not an instance variable, but class
variables aren’t different per object - each object has the same value of that class variable
(technically, they don’t even carry around the variable to begin with; the class itself does
that. Instances just look it up in the class), so changing its value means it would change the
value everywhere, which can lead to unexpected or unwanted results, such as shown below:
class NewClass
@@my_cvar = 0
def my_cvar
return @@my_cvar
end
def my_cvar=(n)
@@my_cvar = n
end
end
obj1 = NewClass.new
obj2 = NewClass.new
p obj1.my_cvar #=> 0
p obj2.my_cvar # => 0
obj1.my_cvar = 1
p obj1.my_cvar #=> 1
p obj2.my_cvar # => 1
As useless as class variables might seem (and frankly, you’ll hardly ever use them), they do
actually have their uses. What if we wanted to count how many instances we’ve ever created
of a class, for instance? We’d have a class variable (independent of any instances) be
incremented by 1 every time the constructor is called.
class NewClass
@@instance_count = 0
def initialize
@@instance_count += 1
end
def self.instance_count
return @@instance_count
end
end
p NewClass.instance_count #=> 0
NewClass.new
p NewClass.instance_count #=> 1
NewClass.new
NewClass.new
NewClass.new
p NewClass.instance_count #=> 4
Global variables
Another type of variables are global variables. As the name implies, these are global can
be accessed from everywhere, at any time, in any scope. Unlike constants that are tied to a
specific scope, global variables are everywhere. Where instance variables use @ to
differentiate them from other variable types, global variables are preceded by a $ sign.
$my_gvar = 0
$my_gvar = 1
def change_that_var(n)
$my_gvar = n
end
change_that_var(2)
class NewClass
$my_gvar = 3
end
Global variables are generally more memory-heavy though, so you shouldn’t go overboard
with them.
9: Hashes
Introducing yet another data type: Hash. Hashes are very much like arrays in that it’s a list of
objects with a specific location in the array/hash. The key difference (pun intended) is that
objects inside arrays are always at a specific index which is a whole number between 0 and,
well, infinity, whereas the “indexes” of objects inside a hash can be pretty much any object: a
symbol, a string, a number, an array, another hash, or even a class.
Hashes are denoted by typing a bunch of key => value structures, separated by a comma,
within two curly brackets. Here, key stands for an object of some kind, and value stands for
an object of some kind. These don’t have to be the same.
hash = {
"start" => "hello world!"
}
print hash #=> {"start"=>"hello world"}
print hash["start"] #=> "hello world"
Like most other things in Ruby, spaces and newlines are almost always disregarded. It doesn’t matter how many
or how little whitespace you have.
In this example we see a hash with one key-value pair. The key is “start”, and the value that
belongs to that key (in arrays, index) is “hello world!”.
Those keys aren’t limited to just strings (the logical choice, because they’re more descriptive
than, say, 482 as a key), but you can also use other data types:
hash = {
"one" => 1,
2 => "two",
:three => ["three",3]
}
Now that we know this, we could technically also make a fake “array” with hashes as long as
we use numbers for the keys.
hash = {
0 => "zero",
1 => "one",
2 => "two"
}
print hash[0] #=> "zero"
print hash[1] #=> "one"
print hash[2] #=> "two"
array = [
"one",
"two",
"three"
]
print array[0] #
=> "zero"
print array[1] # => "one"
print array[2] # => "two"
From this point it’s basically like a regular ol’ array. We can use the keys to access the
objects in the hash and change their values:
hash = {
"three" => 3,
}
...Or we can add to it (if a key doesn’t exist in a hash, it returns nil):
hash = {}
So as you can see, Hash#keys nicely organizes the keys present in a hash. Important to
note though is that the order of Hash#keys is not constant. As you add/remove/change
the hash, this order may change, meaning that you can’t rely on “the first array element is
“james”” and the like. It might return ["trevor", "james", "carl"] later on in your code,
for instance.
However, you can still iterate through this array as long as you make use of the keys
themselves.
hash = {
"a" => 1,
"b" => 2,
"c" => 3
}
for key in hash.keys
print key
end
This will print “a”, “b”, and “c” in an unknown order. You can then use hash[key] to retrieve
the value of whichever key you’re currently iterating over. Another possibility is to store the
keys somewhere before iterating, and then use a for-loop with a range.
hash = {
"a" => 1 ,
"b" => 2 ,
"c" => 3
}
keys = hash.keys
for i in 0...keys.size
print keys[i] #=> "a", "b", or "c"
print hash[keys[i]] #=> 1, 2, or 3
end
A bunch of useful methods for hashes
For all code in the list below, assume the following applies:
hash = {
"one" => 1,
"two" => 2,
"three" => 3
}
Checking keys
print hash.has_key?("three") #=> true
print hash.has_key?("four") #=> false
print hash.key(2) #=> "two"
print hash.key(3) #=> "three"
print hash.key(4) #=> nil
Deletion
print hash["one"] #=> 1
hash.delete("one") # Give it the key you want to delete
print hash["one"] #=> nil
Checking values
print hash.has_value?(3) #=> true
print hash.has_value?(4) #=> false
hash["something"] = 4
print hash.has_value?(4) #=> true
print hash.values #=> [1, 2, 3, 4]
The reason there’s no Hash#value (despite there being a Hash#key), is because you can easily fetch a value
based on key with hash[key] already. This isn’t possible with fetching a key based on a value, which is why
Hash#key exists.
10: Exception handling
To be added soon.