Learning Ruby on Rails: Part I - The Language

Most of my programming experience has been in PHP, JavaScript/JQuery, and VB.NET. When I worked in Support at Code Climate, it became obvious to me that I had picked the wrong languages, and that Ruby was the better choice. With its super simple and legible syntax, Gem support, and stellar framework, I was excited to dig in!

A few years back I dabbled in trying to learn Ruby on Rails, but I made the mistake of trying to teach myself both the language and the framework at the same time. That led to mostly confusion and frustration. I've heard many different opinions on this subject, but I now strongly believe that you should have a firm grasp of the language before trying to learn one of its frameworks.

For the past year or so I've been focusing solely on Ruby, primarily by consuming Team Treehouse's Learn Ruby track. It's a 20-hour course in all and I'm stoked to have finally completed it! Next up: Rails!

While moving through the Ruby course I took copious notes. I thought it would be helpful to share those notes here. Enjoy!

All notes below were written by, based on the Team Treehouse course mentioned above. Red text indicates a key concept.

  • To prompt the user to provide data at runtime, use gets.
  • To execute code within a string, use my_variable = "I am #{code to execute}".
  • In the bullet-point above, using single- (versus double-) quotes around the string modifies the behavior. Double-quotes would run the code in between the { and } characters. Single-quotes would treat everything as a string, without running any code. This is called interpolation.
  • Ruby makes it easy to define a string across multiple lines.
  • my_string = <<-HERE
    I am a string
    that is defined across
    multiple lines!
    HERE
  • In the example above, the word HERE could be any string that you like.
  • When defining a string, you can use special characters, such as \n (new line) or \t (tab).
  • Ending a method with a ! changes the original variable. For example, my_string.upcase! permanently modifies the my_string variable. On the other hand, my_string.upcase will not modify the original my_string variable.
  • You can identify what class an object is a member of using object_name.class.
  • Ruby methods must start with a lowercase character.
  • To create a new method, use: def my_method
     # Awesome Code Here!
     # It is common to indent code within a method by two spaces.
     # Not by a tab.
    end
  • The number of arguments that a method accepts is called arity.
  • In IRB, you can load in a file, to leverage its methods and variables. To load a file, use load "./file.rb".
  • The common commenting conventions are to do the following: Use one space between the leading # character of the comment and the text of the comment. Comments longer than a word are capitalized and use punctuation. Use one space after periods.
  • You can increment a numeric variable using my_variable += 1. This works with other operators as well, such -= and *=. This also works with strings, such as my_string += "foo", which would concatenate the current value of my_string with the value foo.
  • Ruby uses the following operators:
    1 == 1 # equals
    1 != 2 # Does not equal.
    && # And
    || # Or
  • Use two spaces per indentation level (aka soft tabs). No hard tabs.
  • To perform an else/if, use the keyword elsif, without the e. You can have multiple elsif statements in the same if block.
  • An alternative to if blocks are case blocks. For example: case answer when "yes"
        puts "Your answer was 'yes'!"
      when "no"
        puts "Your answer was 'no'!"
      else
        puts "Your answer was not recognized."
    end
  • Within an if block, add a ! character to the front of a condition to mean "NOT true." For example, if !(gender == "male). In this example, the condition will be satisfied if gender does NOT equal male. The condition must be surrounded with ( and ) in order to use !.
  • If you use multiple operators on the same line (e.g., variable = 1 + 3 * 3), Ruby uses a precedence system to determine which operators to execute first. The order is anything in parenthesis, then exponents, multiplication and division, and then addition and subtraction.
  • If a method returns a true or a false, it is a standard a Ruby convention to name the method with a ? at the end. For example, def divisible_by_3?(number).
  • Ruby arrays can contain values of different types. For example, you could create an array that contains both numbers and strings.
  • You can also store variables in an array. For example, my_array = [my_variable, "2", "3"].
  • The Ruby equivalent of var_dump or print_r (for an array) is .inspect. For example, my_array.inspect.
  • To create an empty array, use my_array = [].
  • There are a number of different ways to add values to an existing array. For example:
    my_array = ["a", "b", "c"] #Create the array.

    # Add a single new value to the end of the array.
    my_array << "d"
    my_array.push("e")

    # Add multiple new values to the end of the array.
    my_array += ["f", "g"]

    # Add a new value to the beginning of the array.
    my_array.unshift("i")

    # If you inspected this array, it would now return:
    # ["i", "a", "b", "c", "d", "e", "f", "g"]
  • Though the parenthesis are technically optional, it is a standard Ruby convention to include the parenthesis when a calling a method, unless the method doesn't accept any arguments.
  • To get a specific value in an array, use my_array[i], where i is the item that you want. An alternative method is to use my_array.at(i).
  • To get the first item in an array, use my_array.first. Alternatively, to get the last item, use my_array.last.
  • To identify the number of items in an array, use my_array.count.
  • To identify the number of items a particular item exists in an array, use my_array.count("foo").
  • To identify if a value exists in an array, use my_array.include?("foo").
  • To remove the first item in an array, use my_array.shift. Alternatively, to remove the last item in an array, use my_array.pop.
  • A hash can be thought of as an array that stores key/value pairs. Both the key and the pair can be virtually any data type.
  • It is common (almost even standard) for the key in a hash to be a symbol. For example, my_hash = { :name => "jonathan" }. This is because: Using symbols not only saves time when doing comparisons, but also saves memory, because they are only stored once (source).
  • When using symbols as keys in a hash, you don't need to use the => character. Instead, you can use the following syntax: my_hash = { name: "jonathan", sex: "male" }
  • To create an empty hash, use my_hash = {}.
  • To create a hash with items in it, use my_hash = { "name" => "jonathan" }.
  • To retrieve a specific value from a hash, use my_hash["key"] (in this example, the key is a string; if the key was instead a symbol, you would use my_hash[:key].
  • To add a new value to an existing hash, use:
    my_hash = { first_name: "Jonathan" } #Create the new hash and store a single key/value pair.
    my_hash[:last_name] = "Powers" # Add a second value.
  • To remove an item from a hash, use my_hash.delete(:sex).
  • To list all of the keys in a hash, use my_hash.keys.
  • To check if a hash has a specific key inside of it, use my_hash.key?(:key_name).
  • To list all of the values in a hash, use my_hash.values.
  • To check if a hash has a specific value inside of it, use my_hash.value?("Jonathan").
  • A basic loop is:
    loop do
      # Do something.
      # This loop will keep iterating until you press CTRL + C.
    end
  • To break from a do loop, call break.
  • You can put an if statement on a single line, such as: puts "Foo" if x == "bar".
  • An easy way to identify if a particular string exists in a string is using:
    foo = "bar"
    result = foo.index(/bar/).nil?

    # The `index` method returns the index of the first occurrence of the given substring or regex pattern.
    # If the substring/pattern is not found, `index` returns nil.
    # Calling `nil?` returns a boolean indicating whether or not the substring/pattern was found.
  • An example of a while loop:
    x = 0
    while x < 11
      x += 1
      puts x
    end
  • An example of an until loop:
    x = 0
    until x == 100
      x += 1
      puts x
    end
  • To iterate over a hash, use:
    places = {"Houston" => "Texas", "Atlanta" => "Georgia" , "New York" => "New York"}

    # Iterate over the key and the value.
    places.each do |key, value|
      puts key + " - " + value
    end

    # Iterate over just the key.
    places.each_key do |key|
      puts key
    end

    # Iterate over just the value.
    places.each_value do |value|
      puts value
    end
  • You can call the times() method on any integer, which will run a loop that many times. For example:
    10.times do |count|
      puts count
      # count (which is optional) returns the iteration number.
    end
  • The following is an example of a for loop:
    x = 1
    y = 5

    for item in x..y do
      puts item
    end
  • You can also use a for loop on an array. For example:
    states = ["Texas", "New York", "Idaho"]

    for state in states do
      puts state
    end
  • A common Ruby convention is to prefer an each loop to a for loop.
  • There is an important distinction between an each and a for loop. The variables created within a for loop will remain available to you to access after the loop exits. On the other hand, variables created within an each loop will not.
  • When defining a method, you can set default values for arguments. For example, the method below sets c to 25, unless the method's caller explicitly overrides this value.
    def some_method(a, b, c=25)
    end
  • Class names always start with a capital letter. For example, Dog.
  • To instantiate a new object, call Dog.new.
  • It is a Ruby convention to keep class names singular (e.g., Dog not Dogs). In addition, if the class name is two words, do as follows: PromotedEvent (not Promoted_Event or promotedEvent).
  • When you initialize an object, its initialize method is automatically called. For example:
    class Dog
      def initialize
        puts "Woof!"
      end
    end

    my_dog = Dog.new # The string "Woof!" will be printed.
  • To see all methods belonging to a class, call an object's .methods method.
  • You can define instance variables that live within the scope of a class definition. Instance variables will be available to all of the logic defined within the class (e.g., methods in the class can access instance variables). To create an instance variable, define an initialize method at the top of the class. Within initialize define your variables. The name of instance variables always start with @. For example:
    class Dog
      def initialize
        @name = "Fido"
      end
      def greet
        puts "Hi! I'm #{@name}"
      end
    end

    fido = Dog.new
    fido.greet # The string "Hi! I'm Fido" will be printed.
  • Within a class' initialize method, you can define arguments that the class will require during instantiation. Furthermore, within initialize you can set the value of a required argument to an instance variable. For example:
    class Dog
      def initialize(name)
        @name = name
      end

      def greet
        puts "Hi! I'm #{@name}"
      end
    end

    ralph = Dog.new("Jim")
    ralph.greet # The string "Hi! I'm Jim" will be printed.
  • In many cases, you'll want to access an object's instance variables outside of the class definition. For example, sticking with the code sample above, what if you wanted to access a specific Dog's name after you had instantiated it? Based on how the code above is written, you would not be able to just call .name on the object. In this situation, you'd need to use Ruby's attr_reader keyword, as seen in the example below.
    class Dog
      attr_reader :name

      def initialize(name)
        @name = name
      end

      def greet
        puts "Hi! I'm #{@name}"
      end

    end

    ralph = Dog.new("Jim")
    puts ralph.name # The string "Jim" will be printed.
  • When you use attr_reader with an attribute (as seen in code example above), you no longer need the @ character when referring to the corresponding instance variable (assuming you're within the class' definition). In other words, within the class, you can simply refer to name instead of @name.
  • An instance variable that is visible outside of the class definition is called an attribute. For example, in the code sample within the code block above, name is an attribute.
  • For testing/troubleshooting purposes, you can load all code defined within an .rb file right into IRB by simply calling load "./my_file.rb". After doing so, you can then access any classes, methods, or other code defined within the file, right from IRB.
  • Similar to attr_reader, you will also often need to modify a class' attributes. To do so, use attribute_writer within the class' definition. For example: class Dog
      attr_reader :name
      attr_writer :name

      def initialize(name)
        @name = name
      end

    end

    dog = Dog.new("Jim")
    puts dog.name # The string "Jim" will be printed.
    dog.name = "Mike"
    puts dog.name # The string "Mike" will be printed.
  • Within a class' definition, if there are attributes that you will need to both access and modify, you don't need to separately use attr_reader and attr_writer. Instead, you can simply include attr_accessor in the class' definition, which will allow for both accessing and modifying. In other words, attr_accessor combines and replaces both attr_reader and attr_writer. For example:
    class Dog
      attr_accessor :name

      def initialize(name)
        @name = name
      end
    end

    dog = Dog.new("Jim")
    dog.name = "Mike"
    puts dog.name # The string "Mike" will be printed.
  • Within a method definition, the output of the last line will automatically become the method's return value, even if you do not include the return keyword. This is referred to as implicit returns.
  • Within a class' definition, it's important to understand the scope of any given variable. An instance variable is defined at the class-level, and is accessible through out the entire class (e.g., all of the class' methods and other attributes can access an instance variable). On the other hand, a local variable has a much more limited scope, as it is only accessible within the method in which it is defined. Remember that instance variables are usually created within a class' initialize method, and their names always start with a @ character. Local variables, on the other hand, are defined within a method, and have no special naming convention.
  • To see all instance variables in a particular class, call the object's .inspect method. For example, fido_the_dog.inspect.
  • When you create your own class, it contains both the methods that you explicitly create PLUS some built in methods that Ruby assigns to all classes. For example, all classes have a built in to_s method, even if the class' creator doesn't create it. Keep in mind that you can overwrite any of the built in methods created by Ruby by simply redefining them within your class' definition.
  • When naming Ruby files, use all lowercase letters (e.g., test.rb). Also, if there are multiple words in the file's name, separate the words with an underscore (e.g., bank_account.rb).
  • When writing an if statement, the parenthesis are optional, and it is a standard Ruby convention to omit them. For example:
    # This is good.
    if 1 == 1
      # Do something cool
    end

    # This is less good.
    if(1 == 1)
      # Do something cool
    end
  • To check if a variable is nil, call nil?. For example, dog.nil?.
  • To check if a variable exists, call defined?. For example, if defined?(foo). In this example, the if condition will only be satisfied if the variable foo exists. Note that defined does not return true if the variable exists -- it returns the type of the variable that foo is (e.g., local-variable). To instead return a true or false, you can do the following:
    foo = 'bar'
    puts defined?(foo).nil? # Returns true
  • It is sometimes necessary to assign a value to variable only if that variable does not already have a value. To do so, you can use an if statement, or you can alternatively use the following shorthand: foo ||= "bar". In this example, the variable foo will only be assigned the value bar if foo does not already have a value.
  • To reference code that lives in a separate file, use require "‹filepath›". For example, require "./test.rb".
  • In general, methods that end in ! characters indicate that the method will modify the object it's called on. Ruby calls these "dangerous methods" because they change state that someone else might have a reference to. For example, let's say that a class has an instance variable name. Let's also say that the same class has a method upcase_name!, which changes the instance variable name to be uppercase. In this example, the method's name, by convention, should end in ! because calling this method will change that instance variable, and that change will affect all other objects/places/programmers/etc. that may use the instance variable. Note that the ! is a style/readability convention, not a technical requirement within Ruby.
  • This is obvious, but still worth noting... If you create your own class, and then instantiate that class as an object, you can store that entire object into an array, or into a hash, or even require that object be passed into a custom method that you create. In other words, the classes/objects that you custom create are just as valid/usable as the classes/objects that are built into Ruby, like the string or fixnum class/object.
  • You can use the multiplication operator (*) against strings to repeat them. For example, if you wanted to show the - character 30 times (e.g., to act as a horizontal ruler), you would do the following: puts "-" * 30.
  • Ruby offers the keyword unless to be used in place of, or along side, an if. For example:
    foo = "bar"

    unless foo == "bar"
      puts "not bar" # The string "not bar" will be printed.
    else
      puts "bar"
    end
  • A block in Ruby is very much like a method: it can take some arguments and run code for those arguments.
  • There are two ways to define the starting and ending points of a block. First, you can use the do and end keywords. Second, you can alternatively put the block's code inside of { and } characters. The Ruby convention is to use braces for single-line blocks (i.e., where all of the code within the block will be written on a single line) and to use do and end for multi-line blocks. Below are examples of both:
    # Example using do and end.
    i = 0
    loop do
      i += 1
      puts i # 1, 2, 3 will be printed to the screen.
      break if i == 3
    end

    # Example using braces.
    i = 0
    loop {
      i += 1
      puts i # 1, 2, 3 will be printed to the screen.
      break if i == 3
    }
  • It is important to understand that in the example above the loop method is being called, and it is then being passed a block as an argument. The loop method then acts on that block. This is a good example of the ability in Ruby to pass blocks into methods, and then have those methods execute the code defined with the blocks. This provides you with a lot of flexibility.
  • You can pass arguments into a block using the following syntax: [1, 2, 3].each {|n| puts "Number #{n}"}. In this example, the |n| is a block parameter, and its value in this case is going to be each of the numbers in turn, in the order they are listed inside the array. So for the first iteration of the block the value of n will be 1; then for the second iteration the value will be 2, and then 3.
  • The following is another example of a block, which calls the times method (on a fixnum object) to determine when the block completes. In this example, the times method is being passed a block as an argument.
    # Will print "Hi!" three times.
    i = 3
    i.times { puts "Hi!"}
  • As mentioned above, you can pass blocks into methods that you create. When doing so, you instruct the method to execute the block's code by using the yield keyword within the method's definition. For example:
    def my_method
      puts "First line of my_method."
      yield
      puts "Last line of my_method."
    end

    my_method { puts "I'm defined within a block!" }

    # The code above will print the following:

    # First line of my_method.
    # I'm defined within a block!
    # Last line of my_method.
  • In the example above, note that the method doesn't necessarily accept the block as an argument. Instead, you simply call the method and then define a block. If the method's definition contains a yield keyword then the method will execute the block's code. If the method's definition does not contain a yield keyword, then the block's code will be ignored.
  • Blocks cannot contain the return keyword (if they do, an error will occur). Instead, a block always uses an implicit return (that is, the value generated by the last line in the block is always the return value).
  • When you yield to a block within a method, you can pass back arguments to that block. The block can then leverage those arguments within its execution. For instance, in the example below, the block is outputting variables that are set within the method.
    def say_hi
      puts "What's your first name?"
      first_name = gets.chomp

      puts "What's your last name?"
      last_name = gets.chomp

      yield first_name, last_name # <-- Passed to block
    end

    say_hi do |fname, lname|
      puts "Hi there, " + fname + " " + lname
    end
  • In the example above, it's important to note that the variables defined within the block (fname and lname) are mapped to the variables yielded within the method (first_name and last_name). This appears to work based on the left-to-right ordering of the variables in both the method and the block. In other words, fname maps to first_name solely because they are both the first variable defined in both the method and the block, not based on their names.
  • In the examples above, the block is not passed to the method as a required argument -- it's passed optionally when the method is called. Alternatively, in the method's definition, you can declare the block as a required argument, which must be passed when the method is called. To do so, you declare the argument like any other (in parenthesis), but you add a & character in front of the argument's name. For example:
    def say_hi(greeting, &my_block)
      # To refer to the block, use `my_block`.
    end
  • When you declare the block as an argument in the method's definition, you can use my_block.call instead of yield. You can also pass arguments to the .call method, and those arguments will pass into the block. For example:
    def say_hi(greeting, &my_block)
      puts greeting
      puts "First Name:"
      first_name = gets.chomp

      puts "Last Name:"
      last_name = gets.chomp

      my_block.call(first_name, last_name)
    end

    say_hi("Fancy seeing you here!") do |fname, lname|
      puts "Hi #{fname + " " + lname}!"
    end
  • When reading the Ruby documentation, it's important to understand how Ruby's built-in methods are described, with respect to arguments and blocks that they accept. For example, review the documentation for the integer class' downto method. The documentation explains that downto accepts both a non-block argument (named limit) as well as a block argument (which the documentation refers to as block). For example:
    age = 34

    age.downto(29) { |x| puts x }

    # Will print the following:
    # 34
    # 33
    # 32
    # 31
    # 30
    # 29
  • In the example above, and as explained in the Ruby documentation, if you do not pass a block when calling downto then the method will simply return an enumerator object.
  • From what I can tell, you cannot pass blocks defined within loops to methods as arguments. For example, you could not pass the following into a method: while i < 10 do puts i end.
  • When yielding to a block (within a method's definition), you can pass an argument of self back to the block. This argument is the instance of the class (the class that owns the method). For example:
    class Dog
      attr_accessor :breed

      def initialize
        @breed = "Westie"
      end

      def bark
        puts "Woof!"
        yield self if block_given?
      end
    end

    fido = Dog.new

    fido.bark do |d| # d is the instance of the class
      puts "Bark, bark, bark!"
      puts d.breed
    end
  • The term domain modeling refers to the process of lying out your program's functionality into different modules, classes, methods, and variables. It's the overall object oriented design of your program.
  • It can be handy to write an if statement in the following way:
    do_something if 1 == 1
  • The gsub method (which is a member of Ruby's built-in String class) does a find/replace. For example:
    test = "Fancy 123 Pants"
    puts test.gsub(" 123 ", " ") # Displays "Fancy Pants"
  • You can persist Ruby objects to a .yml file, and then reload them from the file. This is somewhat complicated topic that is not covered in my notes here, but it's important to know that it's possible to do.
  • A module can't have instances, because a module isn't a class. However, you can include a module within a class definition. When this happens, all the module's instance methods are suddenly available as methods in the class as well. They get mixed in. In fact, mixed-in modules effectively behave as superclasses.
  • In the example code below, a module named Animal contains a method named die. This module is then mixed into to the Dog class (via the include keyword). By doing so, all instances of Dog can now call the die method, even though this method isn't defined within the Dog class.
    module Animal
      def die
        puts "Oh no. I'm dead!"
      end
    end

    class Dog
      include Animal
    end

    dog = Dog.new
    dog.die
  • It is a Ruby convention to name modules in the same way that classes are named. That is, they each word starts with a capital letter, via camel-casing.
  • Constants are like variables, but they cannot change (if your program changes a constant, Ruby will throw a warning). You can define a constant in a module or in a class. When doing so, the standard Ruby convention is to name the constant using all upper-case, and to use under-scores in place of spaces (an uppercase first character is what identifies the object as a constant to the Ruby interpreter -- the casing of the other characters in the name aren't relevant to the interpreter). For example, the following module defines two constants.
    module Animal
      VERSION = 1.0
      OWNER = "Jonathan"
    end
  • To access a constant outside of its module or class, use the following syntax:
    module Animal
      VERSION = 1.0
      OWNER = "Jonathan"
    end

    puts Animal::OWNER # Will display "Jonathan"
  • Below are examples of constants defined in both a module and a class.
    module Animal
      OWNER = "Jonathan Powers"
    end

    class Dog
      include Animal
      VERSION = 1.0
    end

    puts Animal::OWNER # Outputs "Jonathan Powers"
    puts Dog::VERSION # Outputs 1.0
  • One important point to understand is that Ruby classes in an of themselves are constants. For example, note how the following class names start with an uppercase character: Array, String, Hash.
  • Modules also act as namespaces. In the code below, the Dog class lives within the Animal module (or namespace). Note how :: characters are used when creating a new instance of Dog -- these are required when a class is stored within a module.
    module Animal
      VERSION = 1.0

      class Dog
        def bark
          puts "Woof!"
        end

        def print_version
          puts VERSION
        end
      end
    end

    dog = Animal::Dog.new
    dog.bark # => Woof!
    dog.print_version # => 1.0
  • In the example above, note that the class definition does NOT have include Animal. When a class lives within a module, there is no need for the include.
  • If you have two classes defined at the same level, in the same namespace, then you don't need to use the :: characters. For example, in the code below, the Cat class creates a new Dog object without using ::.
    module Animal
      class Dog
        def bark
          puts "Woof!"
        end
      end

      class Cat
        def kill_dog(dog_to_kill)
          dog_to_kill = Dog.new
          puts "He dead!"
        end
      end
    end

    dog = Animal::Dog.new
    cat = Animal::Cat.new
    cat.kill_dog(dog) # => He dead!
  • You can create multiple levels of namespaces, by storing modules within modules. When doing so, you have to use multiple :: characters to point to the desired class. For example:
    module Animal
      module FriendlyAnimal
        class Dog
          def bark
            puts "Woof!"
          end
        end
      end

      module MeanAnimal
        class Cat
          def purr
            puts "Meow..."
          end
        end
      end
    end

    dog = Animal::FriendlyAnimal::Dog.new
    dog.bark # => Woof!

    cat = Animal::MeanAnimal::Cat.new
    cat.purr # => Meow...
  • Objects that are built-into Ruby (like String and Array) are part of the Ruby Core, and do not have to be required in order to be used. On the other hand, the Ruby Standard Library is a large collection of internal libraries that ships with every Ruby implementations, which offer additional, commonly-used functionality on top of the Ruby Core. Unlike core objects, standard libraries must be required specifically if needed.
  • The Ruby Standard Library is made up of packages. To see a list of all the available packages, see the sidebar on this page. To use a package, you must require it. For example, to use the CSV package, you would do the following: require "csv".
  • Be aware that Ruby Core has a built-in Math module that provides a lot of mathematical functionality. For example, it provides a constant named PI and a method named sqrt (short for sqaure root).
  • A couple of points about the include statement. First, it has nothing to do with files. The Ruby include statement simply makes a reference to a named module. If that module is in a separate file, you must use require to drag that file in before using include. Second, a Ruby include does not simply copy the module's instance methods into the class. Instead, it makes a reference from the class to the included module. If multiple classes include that module, they'll all point to the same thing.
  • Though this is not a hard and fast rule, in Ruby class names tend to be nouns, whereas module names are often adjectives (e.g., Stack versus Stacklike).
  • It can often be helpful to mixin Ruby Core modules into your own classes. By doing so, you gain all of the module's functionality, without having to write any extra code. A common example of this is mixing in Ruby's Enumerable class, which exposes a lot of very helpful traversal, searching, and sorting methods. For example, let's say that you have a class called DogPound that contains a bunch of Dog objects. And let's also say that each Dog has a name and an age. What if your program needs to find if any Dogs in the DogPound are of a certain age? Sure, you could write your own method to iterate over each Dog object, and find if at least one meets this criteria, and then return a boolean. However, it would be much easier to simply leverage the Enumerable module's built-in any? method, which does all of the heavy lifting for you. See below for an example.
    class DogPound
      include Enumerable
      attr_accessor :dogs

      def initialize
        @dogs = []
      end

      def each(&block)
        dogs.each(&block)
      end

      def add_dog(name, age)
        dog = Dog.new(name: name, age: age)
        dogs.push(dog)
      end

    end

    class Dog

      attr_accessor :name, :age

      def initialize(hash)
        @name = hash[:name]
        @age = hash[:age]
      end

    end

    dog_pound = DogPound.new

    dog_pound.add_dog("Fido", 11)
    dog_pound.add_dog("Mark", 7)

    puts dog_pound.any? { |dog| dog.age > 5 } # => true
    puts dog_pound.any? { |dog| dog.age > 50 } # => false
  • In the example above, it's not obvious why the each method is being created in the DogPound module (since it is never explictly called. If you were to remove this method definition, however, the code above would error out. This appears to be a standard requirement when mixing in the Enumerable module, as explained here
  • It's important to understand the different between include and require in Ruby. The include method takes all the methods from another module and includes them into the current module. This is a language-level thing as opposed to a file-level thing as with require. The include method is the primary way to "extend" classes with other modules (usually referred to as mix-ins). For example, if your class defines the method each, you can include the mixin module Enumerable and it can act as a collection. This can be confusing as the include verb is used very differently in other languages. The require method is used to load another file and execute all its statements. This serves to import all class and method definitions in the file.
  • You can run code whenever a module is included in (i.e., mixed into) a class. In other words, right when the include method is called within the class, your code will run. To do so, within the module's definition you define a method self.included and and pass it a variable that corresponds to the instance of the class that is mixing in the module. For example: self.included(klass). Note that the word klass here is arbitrary, and can be almost anything -- it is simply a name to use when referring to the class instance. It is a Ruby convention to use klass because class is a reserved word in Ruby and cannot be used. The following is an example of a module that is mixed into two classes.
    module Animal
      def self.included(klass)
        puts "I love being included!"
      end
    end

    class Dog
      include Animal
    end

    class Cat
      include Animal
    end

    dog = Dog.new # => I love being included!
    cat = Cat.new # => I love being included!
  • In the example above, note that self.included is a class method.
  • There is a difference between class methods and instance methods. Class methods are methods that are called on a class and instance methods are methods that are called on an instance of a class. Class methods must be named with self. in front, like self.foo. In the example below, a class method is defined (foo), and in order to call it, you must do so directly off of the class, not off an instance of a class. Alternatively, and also included in the code sample below, an instance method is defined (bar), and it can only be called off of a instantiated instance of the class.
    class Test
      def self.foo
        puts "I'm a class method!"
      end

      def bar
        puts "I'm an instance method!"
      end

    end

    Test.foo # => "I'm a class method!"
    Test.new.bar # => "I'm an instance method!"
  • In addition to include, you can also mix a module into a class using extend. However, these two keywords do not provide the exact same functionality, and it can be somewhat abstract to explain their differences. Essentially, include mixes in the module's objects in a way where each INSTANCE of the class can use them. On the other hand, extend mixes in the module's objects in a way where each instance CANNNOT use them -- instead the actual class itself can use them. Another way to word this is, when using include, you are mixing in instance objects. When using extend you are mixing in class objects. Here is an excellent description of these two keywords.
  • A Struct is a convenient way to bundle together attributes and methods without having to build out a whole class. In other words, a Struct allows you to on-the-fly create something that works just like a class, in it that it has attributes and behavior. Below is an example of how to create a Struct.
    SelectOption = Struct.new(:display, :value) do
      def to_ary
        [display, value]
      end
    end

    option_struct = SelectOption.new("Canada (CAD)", :cad)

    puts option_struct.display
    # Canada (CAD)

    puts option_struct.to_ary.inspect
    # ["Canada (CAD)", :cad]
  • In the example above, the to_ary method is an instance method, available to be called on any instance of the struct/class.
  • Also in the example above, option_struct is a full-blown instance of the SelectOption class, even though you never technically created this class. As a result, you can access the instance's variables (display and value) directly, even though you never wrote an initializer method or called attr_accessor. This is one of the major benefits to using a Struct: it's less overhead than creating a full-blown class. That being said, there are other, more beneficial reasons to use a Struct, though most of them seem to require more experience with Ruby than I currently have to appreciate. Here is a great article that covers when to use a Struct.
  • On Ruby class inheritance (source): "In Ruby, a class can only inherit from a single other class. Some other languages support multiple inheritance, a feature that allows classes to inherit features from multiple classes, but Ruby doesn't support this. [...] The benefit of inheritance is that classes lower down the hierarchy get the features of those higher up, but can also add specific features of their own." Below is an example:
    class Mammal
      def breathe
        puts "inhale and exhale"
      end
    end

    class Cat < Mammal # < instructs Cat to inherit from Mammal.
      def speak
        puts "Meow"
      end
    end

    rani = Cat.new
    rani.breathe
    rani.speak
  • To identify if any given class inherits from a parent class, call superclass, which will return the parent class' name. For instance, sticking with the Mammal/Cat example above: puts Cat.superclass # => Mammal. It appears you can only call superclass on the class itself, not on an instantiated instance of the class (e.g., you could not call rani.superclass from the example above).
  • The built-in File class provides functionality to work with files. For example, the code collects some information from the user and then writes it to a file.
    File.open("example.txt", "w") do |file|
      print "Enter your name: "
      name = gets.chomp
      file.puts "Name: #{name}"

      print "Enter your email: "
      email = gets.chomp
      file.puts "Email: #{email}"
    end
  • Ruby offers a built-in way to serialize objects, letting you store them somewhere and reconstitute them when needed. Ruby calls this kind of serialization marshaling, which is available via the Marshal class. You can call Marshal.dump(object) to "dump" any Ruby object into a variable, or into a file. You can call Marshal.load(object) to load a serialized object back into a variable. In the example below, an object is created, dumped to a file, and then later reloaded into a new variable.
    class Dog
      attr_accessor :name

      def initialize(name)
        @name = name
      end
    end

    fido = Dog.new("Fido")

    File.open('fido_file', 'w+') do |f|
      Marshal.dump(fido, f) # The fido object is saved to a file.
    end

    fido_new = false
    File.open('fido_file') do |f|
      fido_new = Marshal.load(f) # The same object is loaded into a new variable.
    end

    puts fido_new.name # => Fido
  • Similar to marshalling, you can also serialize Ruby objects using the JSON class. To dump an object to JSON, call JSON.dump(). To load a Ruby object from JSON, call JSON.load().
  • YAML is often used for configuration files.
  • When calling YAML.load or JSON.load (or any other similar load method), there are security concerns. Essentially, never load data from a source that isn't trustworthy.
  • The Logger class gives you a quick and easy way to generate and write to a log file. In the example below, a log file is created whenever a new instance of Dog is initialized. In addition, the log file is added to whenever an instance of Dog sends out a greeting.
    class Dog
      require "logger"

      attr_accessor :name, :logger

      def initialize(name)
        @name = name
        @logger = Logger.new("#{name}.txt")
      end

      def greet
        puts "Hi, I'm #{name}"
        logger.info("#{name} sent out a greeting. What a friendly puppy!")
      end
    end

    fido = Dog.new("Fido")
    fido.greet
  • In the example above, logger.info is called. In this context, info is one of the levels that Logger supports. The other levels include error and debug, amongst others.