Break into Ruby
The only pre-requisite is ‘Break into Julia’. This chapter aims to persuade you, a beginner or an intermediate-level programmer, to learn Ruby, a fine language.
Here we go.
Everything is an Object
Now then, in Ruby, variables are disloyal:
thing = 34
thing = "what?"
thing = true
thing = 3.2
thing = [1, 4, 4]
thing = thing.nil?
In other words, our variable thing
can assume any data type. First it became an int, then a string, then a boolean, a float, a list (called array in Ruby) and finally, a result of evaluating whether the thing
object is a nil or not? In this case, thing
is assigned a new boolean value (false
).
This need not be cause for alarm. Ruby also has what are called constants
. A constant is an assignment whose value never changes. By the way, constants are always written in capital letters.
STUBBORN = "you can't change me. Yohoo!"
By contrast, variables are written in small letters and if there are more words, then a snake_case is used:
this_is_it = "I became a snake"
your_birthyear = 1844
Now will briefly turn to objects so that we can talk about methods and functions. But what are objects?
An object can be any object you want it to be. Okay that was not very clear (but it was recursive); objects are things and things are parts of a problem you are trying to solve.
A lemonade stand is an object, but so are lemons, and the cups, sugar, customers, and the lemonade machine; in short, objects are entities that have attributes, or associated actions, or both; we can talk to objects (and they will respond to us with their pre-defined methods), or have objects talk to each other. We can use objects and their attributes without even using their methods.
In Ruby, everything is an object, including a class! In other words, there is something called Object
that is actually a class of all objects, which can be used to spawn new objects. We will return to this topic in the future, but for now, do not confuse this with the following:
Here the .class
method of an object determines what class the object belongs to. We are showing that “hi” is an object of the class String
, for instance.
More specifically, a dog is of class animal. Any dog (object), descends from the animal (class), therefore. The object has methods and properties: a dog has four legs, so this is a property, while a dog can bark, so this is a method.
So first the Animal
class is created, which has its own data: in our example, isMammal() is assigned as a general-class-level method to the Animal
class to ensure that our animal is a mammal (and not an insect, for instance.).
But now, we cannot assign a general-class-level method of barking to the Animal
class, because some of our mammal friends will not be dogs. Thus, we need two other subclasses below the Animal
class:
That is, the animal class can have two other classes of its own, which can have object methods specific to that subclass. Here the cat and the dog are both of the animal class, but the dog is of the canine class, while the cat is not. Hence, the dog object created from the canine class will have a bark method, but any animal instantiated from the feline class, won’t.
Because of this sub-class division, the methods that apply to the dog can be safely written because we know they only belong to the dog class. But if there is a method or an attribute that applies to both cats and dogs, then we include those in the general (or super) class, the Animal
class.
Let’s see how we define such a “super” class in Ruby (I have withheld the methods to focus on the inheritance:
class Animal
end
class Canine < Animal
end
class Feline < Animal
end
Both Canine
and Feline
classes inherit attributes and methods from the Animal
class. The <
sign ensures that. (Please note that class names always start with a capital letter.).
Now, let us define the isMammal()
(which checks if any objects created from the animal class are mammals or not) method within the Animal
class. To do so, we define a function and place it inside the body of the class. Here is how a function is defined in Ruby:
def reversor(inpt)
inpt.reverse
end
We named it reversor
, and gave it an argument called inpt
(name it as you like within limits); then, since we want the input to this function to be reversed. We use Ruby’s in-built .reverse
method. We close off the function with the end
and there you have it.
By the way, Ruby functions (and methods) are written in small letters or snake_case if needed, and they work just like functions in Python or any other mainstream Algol bastard. Also, to reiterate, classes are created with the class
keyword, but methods and functions are created with the def
keyword. Since, in Ruby, everything is an object, so every function is actually a method. More on this later.
Now, our first task is to place an isMammal
function inside the Animal
class. The function (or method) should then output a Boolean true
when it is called. I will do this one for you, so you can add the other methods for Canine
and Feline
classes yourself:
Explanation: so first, after we defined the Animal
class, we directly called the isMammal() method on it. You see we fail. That’s because class methods cannot be called on the class itself, unless an instance of that class is created.
An instance of a class is simply an object that is born from the factory of that class. In Ruby, we do this by:
some_object = SomeClass.new
Also notice that we can save the newly-minted object instance into any variable name we like as long as it is written in snake_case small letters. Again, note how the class name is starts with capital letters and a CamelCase is used if the word is compound. At the end of the class, a dot is appended to make it ready to use Ruby’s in-built new
method that gives birth to new objects.
As you can see in the screencast, the object has a unique address in the computer. Now, we can call the class method on this new object. You also notice that we can call the method both with object.method
and object.method()
which is a unique attribute of Ruby–a showcase of its supreme flexibility. (You can’t do this in Python, for instance).
Finally, in Ruby, we can put out or display a string value, by doing so:
puts "hi, I am Savage"
But other methods also exist and we will cover those when the need arises. For now, in the body of Animal
class, you notice how I went for puts true
but then changed my mind and only wrote true
. Your mini-task is to check what is the class of true
when the function body uses puts true
versus when we merely return true
without puts
.
In Ruby, the function return
keyword is optional. So:
def greetbot(name)
return "Hi, #{name}"
end
def greetbot(name)
"Hi, #{name}"
end
are the same and output the same thing. By the way, if you have noticed, we just used string interpolation, which is using pre-defined variables within our string for a combined output. Unlike string interpolation in Python, Ruby’s is elegant and very easy on the eyes. (So is Kotlin’s).
If you have installed irb, you will also see how irb suggests which methods go with with object. Once I type a part of the name of the object, auto-complete is done by pressing the tab key on the keyboard. Then, typing a .
after the object name presents a list of all possible methods that can be called on this object.
Now go ahead and add the methods we just mentioned to Canine and Feline classes and call them, to ensure they work. Next, create a subclass that inherits from the Animal
class and run the isMammal
method on it to see if it works (whether the new object of the subclass is also a mammal?).
Alright, we have seen a general overview, but we need to dig deeper.
That is, you recognize the need for objects because the world is also made up objects. Problems from the real world can be mapped to solutions are written in computer language–in programming objects. What is not immediately clear is that the real world is not only made up of objects.
In other words, although objects are there, they are groupable according to a certain class or category. Why is there a need for this in the real world? Consider the following statement:
Not a living thing could be found in that city.
We have used a class here, the class of all living beings, which includes animals, humans, trees, et cetera. We saved much time and got our point across, prevented misunderstandings, and so forth. In programming, the same is true: we use classes so that we don’t have to repeat ourselves, avoid confusion and mess, and organize our program accoding to the logic of the solution it provides.
Functions do the same thing, but at a slightly lower level. That is why we can define functions within classes. So a function is an abstraction but a class is an abstraction over functions.
We will briefly pause our class and object discussion so that we can focus on some other details; after we cover these details, we will combine them with our knowledge classes and objects.
Earlier in the introduction we mentioned that basic familiarity with programming is assumed. This is hard to evaluate, so I will err on the side of caution and provide a gentler lesson than otherwise. Thus, before going into the next section, let’s say you have a program that accepts or requires an input from the user; the user is prompted to enter a number within some range. Then, the function displays a list of all multiples of this number.
Use your favourite language or any language you have learned before to write this, first, so that we can see its Ruby version. If you are rusty, I will list the program below (in Python because it is rather intuitive) but only as a screencast animation so that you can see it in action, and but also, to ensure that you use your own memory to write it (and not peek):
To refresh your memory, we create an empty list called multips. Then we define a function called multipper
and assign a parameter to it; the parameter’s then requests user for input in the range 1 to 20, and converts the input into an Int. After that, it runs a for loop in the range 1 to 20, and if the iterator variable num
is divisible over the input n
, then it appends the result of the loop into the empty list multips
, and then prints the list.
Based on the requirement listed for this book, I expect you to be able to do this on your own, but I have included this bit only just in case, since there is much variability in the definition of an “advanced beginner”. In order to assertain whether you are truly an advanced beginner, take a good look at the above Python animation and see if you can find something odd?
If not, here is a hint: why is the multips
list defined outside the function? If you cannot answer that, head back to your programming 101 text, or google the concepts of local and global scope. (By the way, the multips
empty list can also be written in the body of the multipper
function and the outcome will still be the same.).
Finally, variables should be locally scoped as much as possible. We will come to why and how of this point later on in our jorney. Now, we since we have already seen functions are defined in Ruby, let’s see how to do a for loop, a mod, a list append, and a range in Ruby.
I will list each process separately, and then require you to put them together so that you create a Ruby version of the multipper
function. Let’s start with the Python input(n)
in-built function’s equivalent. In Ruby, we have gets
. Observe the following animation so that my explanation makes sense:
Look for the following in the above animation:
-
gets stands for get string by default. So whatever you supply after the
gets
command, is automatically converted to a string. -
if we add the
.to_i
method to thegets
object, we convert the input we typed to an integer. -
in its original form,
gets
adds a newline to whatever we enter. We can remove that by prepending the.chomp
method to it. -
the order of the chained methods matters. Wrong order leads to error.
Quick: does gets.to_i
need a .chomp
?
After that, let’s see a loop and a range in action:
for i in (1..60)
p i
end
This program will print all the numbers from 1 to 60. Great. And finally, how to append elements to a list:
some_list = []
some_list.push(123)
Now our some_list
looks like this: [123]
. Bear in mind, also, that in Ruby, a list is called an array. Here is a more involved example:
Here’s what we did, in case you need it: we created an array of two integer elements, added three more integers into it, then ran a for loop on it that doubled each of those elements.
Now, you are ready to reproduce the multipper
function I gave you in Python. Work on it and come back to see if your result matches our solution. For reference, here is the Python version once again:
def multipper():
multips = []
n = int(input("Enter any num less than 20 "))
for num in range(1, 20):
if num % n == 0:
multips.append(num)
print(multips)
Additionally, here is an important tip: in Rubny, we use end
to clarify what starts and what ends, and as you can see, Python has none of this. This can frequently cause problems and especially when your code is long and complex. Like Julia, Ruby prevents this. To help you, add an end
to your if
conditional, then close your for
loop with an end
, and then request a print
; use the final end
to close the function.
Here is our version:
def multipper()
multips = []
print "Enter any num less than 20 "
n = gets.chomp.to_i
for num in (1..19)
if num % n == 0
multips.push(num)
end
end
p(multips)
end
The program works and here is a demonstration:
Now that you have done this, it is time to do something slightly more interesting. Imagine someone challenged you to produce the sum of the multiples of 5 and 3 for the first 999 natural (usual, integers) numbers. Given what you have learned so far, how would you go about doing this?
As a hint, consider that .sum
is a Ruby method that sums all the elements of an array. You now have all the knowledge you need to compute this. Go ahead and do this, but if you get stuck, I will provide hints in rather exotic languages (to ensure you sweat a little) before I do so in Ruby:
Here is an implementation in J:
+/ ((0 = 3|n)+.(0 = 5|n)) # n=:>:i.999
Which is saying: the sum +/
of all numbers such that they are less than 1000 (n=: >:i.999)
which are divisible by 3 (0 = 3|n)
or +.
divisible by 5 (0 = 5|n)
. I think this is a pretty good hint.
In case you think I am a weirdo who likes strange languages, here is the same (one of the many possibilities–this is a translation of our J version) in Python:
sum([x for x in range(1000) if x%3==0 or x%5==0])
As I mentioned, the above solution is using list comprehensions, but there are a number of other ways. Here is yet another, also in Python:
sum(filter(lambda n: not n % 3 or not n % 5, range(1000)))
These hints do not help much because they are not how Ruby works or looks like. This, despite the fact that several different solutions exist in Ruby, each one quite different from the other. For instance, consider this solution (but don’t worry if it does not fully make sense. We will cover this in the future.):
puts (3..999).to_a.reject{|n| n%3!=0 and n%5!=0}.inject{|sum,n|sum+n}
or:
def sum_multipper(ary, range)
range.inject(0) do |sum, n|
ary.any? { |i| n % i == 0 } ? sum + n : sum
end
end
puts sum_multipper([3,5], 0..999)
The problem in both cases, is that we have gone for an overkill. So, it is true that you can solve a given problem in any number of ways, especially so in Ruby, but this does not guarantee that the solution is the most optimal one in terms of readability, speed, or even complexity.
For now, all you need to do is:
arr = []
for k in (1..999)
if k % 3 == 0 || k % 5 == 0
arr.push(k)
end
end
p arr.sum
Which, given the problem, is a clear, and pedagogically useful solution (but not the fastest). We will return to program speed in future lessons.
Now we will return to objects, but this time we will focus a bit deeper. You sitting in front of your computer are now communicating with Ruby. You ask Ruby to introduce itself, that is its self. You type:
self
and you get:
main
Which is Ruby’s way of saying “This is me, the main me.”. Technically, main
is a program’s entry way (they key to doors of the program. More later.).
Next, you wonder what Ruby is made of? So you ask:
self.class
Object
Aha. So Ruby itself is a big giant Object! If you have played Fall Out (especially Fall Out 4), there is a robot-companion in that game that accompanies the you, the player, on your missions and also plays the role of a best friend. Ruby is just like that robot. Let’s interact with Ruby. Let’s teach Ruby a new trick, say, how to wink:
def self.wink
p "😉"
end
Now when we call Ruby to perform this trick, it will do so for us:
Now, Ruby is your friend and provides emotional support but you also need an army of robots doing various tasks for you. Fortunately, we can ask Ruby to clone itself and create as many robots as we want!
To do so, we need to ask Ruby to go into the “clone mode”. Technically, we now refer to the Object class (instead of self). Le’s create three robots, each of which will have unique names, and do unique things.
athos = Object.new
porthos = Object.new
aramis = Object.new
Notice that in “clone mode”, Ruby can keep creating as many new objects as it wants, but they are all born from the same source: the Object class. Let’s now add some abilities for the three robots:
def athos.think
puts "provides strategy"
end
def porthos.nourish
puts "cooks"
end
def aramis.defend
puts "unsheats sabre"
end
Let’s play and see what happens:
Notice how we can call the wink
method with or without invoking self
; this is how we know self
is the main Ruby robot. You notice also that calling methods defined on self
won’t work when we chain them to the other three robots that were created from Object (from Ruby’s clone mode). Ditto, methods defined for cloned robots don’t work when called from self
.
Additionally, both cloned objects and self itself, have a number of pre-built methods available to them. You notice that when we call .methods
on athos
. However, whatever method we added to our object, it can also appear when we request a method list. Here, you see think
method for athos
when we scroll through the list of all available methods for athos
.