Coding Practice - Ruby (RubyMonk)

For my first foray into Ruby, I've received some glowing recommendations for a website called RubyMonk. This article will cover my rough notes on their most basic tutorial, the "Ruby Primer." The topics covered are very similar to my other coding practice articles, running from simple variables to creating classes and methods to a bit of file IO.

And a caveat: As with the other coding practice articles, this is written mostly for my own future reference and as a proof-of-work... basically meaning I skipped pretty formatting and grammar. If you're trying to learn Ruby, I'd suggest you try the RubyMonk tutorial for yourself. It's good stuff!


INTRODUCTION - EVERYTHING IS AN OBJECT

In Ruby (like Python) everything is an object. For example, integers are their own object and have built in methods. For example, "2.even?" returns "true", and "true" has its own (boolean) object properties and methods.

Method calls can be chained with successive ".methodName", like "1.next.next" returns 3.

Ruby execution involves inhabiting an object and then interacting with other objects. By default, we inhabit the "main" object.

The keyword "self" returns whichever object is currently being inhabited.

The method ".methods" will make any ruby object return a comma-separated list of all methods (each prefixed with ":") which that object supports. This list can be alphabetically sorted by chaining ".sort" as in "1.methods.sort"

Methods can be called with arguments in the usual way: 2.between?(1, 3) returns true

Ruby implements operators as methods as well, but makes an exception for their syntax. We can write 1.+(2), but we can also just write 1 + 2.

The array syntax is especially interesting, because [] is a method name which can contain arguments within itself... or not. Both of the following produce the same output: "words[1]" and "words[](1)"


STRINGS

Strings are constructed in "literal form." That is, anything enclosed in single or double quotes is interpreted as a string. Double quotes allow escape sequences, single quotes do not.

"somestring".length returns the number of characters in string

#{someCode} will have someCode interpreted and its result inserted into that location, before the rest of the program. This has a lot of uses, one of which is string interpolation.  This is an example of an escape sequence, which will be processed within double quotes but not within single quotes.

a = 1
puts "my text contains the number #{a}"

Another escape sequence is "\n", which inserts a newline

"somestring".include?("substring") will return true if somestring contains substring

"somestring".upcase returns the string entirely in uppercase
"somestring".downcase returns the string entirely in lowercase
"somestring".swapcase returns the string with upper and lower cases swapped

"someString".split(separator) splits the string into a list of strings by separator

"string1" + "string2" will return "string1string2"
"string1".concat("string2") does the same

"somestring1".sub('string1', 'string2') will replace the first occurence string1 with string2 in somestring1.
"somestring1".gsub('string1', 'string2') does replaces every instance of string1 in somestring1.

Regular expressions can be used instead of strings in many of these functions, and are specified between a pair of slashes /regex/

"somestring".match(regex, [pos]) returns the first substring which fits the given regular expression. pos is an optional parameter specifying where the search should start from. We might decide to start at character 9.


CONTROL STRUCTURES

Ruby has the usual set of boolean operators, >, <=, ==, &&, ||, ! and so on.

IF ELSIF ELSE UNLESS

Ruby uses python-style bracketless code blocks. Function definitions and if-statements are closed with "end"

    if condition1
        result
    elsif condition2
        result2
    else
        result3
    end

This can be shortened using the "ternary" phrase, which translates to "if condition1 then result1 else result2"
    
    condition1 ? result1 : result2

Ruby also provides some idiomatic alternatives

    result1 if condition1

    result2 unless condition2


"false" and "nil" equate to boolean false, EVERY other object (including both 1 and 0) evaluate to boolean true.

-----

BASIC LOOPS

Infinite loops will continue running until they hit the "break" keyword. the following would execute result once and then stop.

    loop do
        result
        break
    end

The following allow us to use an integer method to execute a loop that number of times.

    5.times do
        result
    end

------

ARRAYS

Ruby arrays can have mixed types and can change size.

An empty array can be created with either [] or "Array.new"

Values can be inserted with comma-separated balues, like [1, 2, 3]

Values are retrieved from arrays with an index, like myArray[3] or ['a', 'b', 'c'][1].
The retrieval index can also be negative, in which case it will count from the end of the array instead of the beginning. Note that when counting from the ed of the array, we have to start at -1 instead of -0..... because clearly the rules of math are more improtant than the rules of indexes.

New elements can be appended to an array using "myArray << newthing" or "myArray.push(newthing)"

myArray.map { |identifier| identifier + 1 } will perform "identifier + 1" on each element of myArray, with each element of the array accesed by "identifier", and return an array containing the results. You're mapping the original array to a new array according to the given function.

myArray.select { |identifier| condition } evaluates condition for each element of myArray (accesed by identifier) and returns an array containing the elements which evaluate condition to true.

myArray.delete(v) deletes the element with value v and returns v (if successful) or nil (if not found).

myArray.delete_if {|identifier| condition} evaluates condition with identifier for each element of the array, and returns a new array minus elements which satisfy condition.

myArray.shift returns the first element of myArray and removes that element from myArray

myArray.pop returns the last element of myArray and removes that element from myArray

myArray.inject(initial){|memo, element| operation } will perform operation on each element of myArray and store each successive result into memo. The current element of myArray is accessed through the variable "element." If "initial" is not provided, the first element of myArray will be placed into memo and inject will begin executing operation from the second element of myArray.

------

FOR LOOPS

The typical loop for iterating through arrays

    for identifier in myArray
        someCode(identifier)
    end


Alternative syntax for iterating through arrays

    myArray = [1, 4, 62]

    array.each do |identifier|
        someCode(identifier)
    end


-----


HASHES (aka dictionaries aka maps aka associative arrays, etc)

A hash is a collection of key-value pairs. Created with the following syntax...

    myHash = {
        "key1" => value1,
        "key2" => value2,
        "key3" => value3
    }


Getting and setting values work like arrays, just use the key instead of an index
myValue = myHash['key1']
myHash['key1'] = myNewValue

Iterators work like array iterators, but you can access both key and value simultaneously
myHash.each do |myKey, myVal|
    someCode(myKey, myVal)
end


An array of just the keys or just the values will be returned by calling .keys and .values respectively.
allKeys = myArray.keys
allValues = myArray.values


A hash can be set to always give new keys a "default" value unless otherwise assigned. Without a default, keys with no values would return 'nil'
myHash = Hash.new("defaultVal")


Some other syntaxes for creating hashes are...
    myHash = Hash[key1, val1, key2, val2]
    myHash = Hash [[key1, val1], [key2, val2]]   


-------


CLASSES

myObj.class method returns the identifier (actually a member of the 'class' class) of the class to which myObj belongs. Remember that everything is a class, so we can use this method on everything (including literals). 1.class.class returns 'class'. It also means that when we define a function "outside" of a class, it still belongs to a default 'NilClass'.

 

myObj.is_a?(myClass) returns boolean for whether myObj is a member of class myClass

-------

Classes are created with the syntax...

    class myClass
        someCode
    end


Instance variables (aka local properties) are created and accessed with the syntax...

    class myClass
        @myProperty = someValue
    end


Class variables (aka class properties) are created and accessed with the syntax...

    class myClass
        @@myProperty = somevalue
    end

--------

METHODS

Methods are a slightly different in Ruby versus other languages. If everything is an object, then every function (even those declared "outside" of classes) are still within some object and thus are methods.

myObj.method('nameOfAMethod') returns the object representing the method nameOfMethod within myObj

This method object exposes the .call method, which lets us execute the method as if we were calling it normally.


Methods are created for a class with the syntax...

    class myClass
        def myMethod(param1, param2)
            someCode
        end
    end


Method parameters can be given default values with the syntax...

    def myMethod(param1 = value1, param2 = value2)

 

The splat (*) operator allows us to convert between arrays and lists of parameters. All of the following would be valid:

    def myMethod(*param1)
    end

    myMethod(1, 2, 3, 4)
    myMethod(2)


    def myMethod2(param1, param2, param3, param4)
    end

    myArray = [1, 2, 3, 4]
    myMethod2(*myArray)

 

"Optional" parameters can be passed into methods by making the last parameter default to an empty hash. When calling the method with optional arguments, a name must be provided along with the value in the argument. These names can be accessed in the method by using the prefix ":". Unfortunately, Ruby doesn't let you have both splatted arguments and last-parameter-is-a-hash in the same method.
    def myMethod(param1, param2, param3 = {})
        puts param1 + param2 + :somevalue
    end

    myMethod("one", "two", somevalue:"three")
    myMethod("one", "two")

-----------

LAMBDAS

We can store a lambda function into a variable and call it with arguments, like so:

    myVar = lambda { |parameters| codeBlock }
    myVar.call(arguments)


We can also write custom methods which accept a single block of code, similar to the array.inject(){} method we used earlier. This is done using the method "yield" which will execute the passed-in block with whatever arguments you specify.

    def run_a_block(param1)
        yield(param1)
    end

    run_a_block(1) {|number| number+1}

    >> 2


------------

MODULES

Ruby modules let you create groups of methods which can be mixed into any number of classes. Unlike classes, modules do not hold state and cannot be instantiated, meaning the module methods cannot be called directly. Module method must be included in a class, and then called through that class.

Note that because methods can ONLY be called through a class, you can write your module methods as if they are actually in that clas. They can naturally access the local methods (and probably variables) of those classes with no special syntax.... just make sure those methods and variables actually exist.

We include a module into a class using the method "include" with a parameter for the module name.

    module myMod
        "codeCodeCode"
    end
    
    class myClass
        include myMod
    end

    class anotherClass
        include myMod
    end

    puts myClass.someMethod
    puts anotherClass.someMethod
    >> "codeCodeCode"
    >> "codeCodeCode"


Now regarding "everything is an object... things get a little funky. "Module" is the superclass of "Class", and "Object" is the superclass of "Module". Just understand that when somebody talks about a module, they probably mean "a collection of methods" rather than "something that could be any class".

Modules can also include classes, which CAN be instantiated. These are accessed using the "constant lookup operator, ::" This operator allows us to search only within a module, and thus avoid namespace conflicts. This operator can be used and chained to search for any constant (modules, classes, methods, variables assuming they don't change)

    module myModule
        class Array
            someCode
        end
    end

    someVar = myModule::Array.new

Using the lookup operator without naming a parent will use the topmost definition.

    module one
        myVar = 10
        module two
            myVar = 20

            class someClass
                def myMod
                    ::myVar
                end
            end
        end
    end

    one::two::someClass::myMod
    >> 10


IO STREAMS

Ruby's IO class allows us to initialize input/output streams. Note that there are many different kinds of IO, such as StringIO.

    #Open the file "filename" to write, and create a filedescriptor "fd"
    fd = IO.sysopen("filename", "w")

    #Create a new I/O stream using the file descriptor
    IO.new(fd)


Ruby defines the STDOUT, STDIN and STDERR constants, which are IO objects pointing to the input, output and error streams of a typical terminal. While we cannot modify these constants, the default Kernel module provides us with global variables $stdin, $stdout, $stderr which point to thoe same IO objects. By modifying those global variables, we can indirectly modify the constants and redirect their streams.


Ruby also provies a "File" class which gives us more convenient ways of performing file IO.

myFile = File.open(filename, mode) opens up the named file in the given mode and gives us an IOStream so that we can operate on it, and again creates a filedescriptor in myFile.

myFile.read(length, buffer) reads length characters from an IO stream and puts them into buffer. These arguments are optional, and if not provided the method will read the entire file and return it.

myFile.write writes a string to an IO stream and returns the number of bytes written.

myFile.rewind moves the read-position back to the start of the file. An optional parameter lets it rewind to a specific byte.
myFile.seek does the opposite of rewind

myFile.readlines returns an array of all the lines in the IO stream. You can optionally limit the number of lines or insert a custom separator between the lines.

myFile.close closes the file.

Ruby also gives a self-closing version of File.open:
    file.open do |f|
        f.somecode
    end