Wisdom from Eloquent Ruby: From Oh? to Oh Yeah!, Part 2

Wisdom from Eloquent Ruby: From Oh? to Oh Yeah!, Part 2

In Part 1 in this series of posts, I showed you my highlights and notes from Chapters 1 – 9 in Russ Olsen’s fantastic book Eloquent Ruby. This week I’ll be posting them from Chapters 10 – 19. This post will be following along with Part 2 of the book, on classes, modules, and blocks.

Undoubtably, since I’m in the first year of my Ruby journey, I’ll make mistakes in my understanding of the language. Please call me out on them. In learning and in life, if I’m gonna fail, I’d like to fail fast. As it’s the internet, I don’t think this is going to be too much a problem. And as a heads up, the notes in this post are a little longer than in the first post. As I got further into the book, I started encountering newer material I didn’t have a complete grasp on and therefore my notes got a little longer, especially as I tried to create my own code examples (based on Russ’) to give me a firmer grasp on the topic.

Finally, these notes truly pale in comparison to the caliber of content in Russ’ book. This is meant to help myself and others as a study aid but is in no way a substitute. If you find any of this useful, go grab the book. You won’t regret it. I had a chance to talk with Russ through some emails before I started writing this. He seems to be a genuinely great guy that truly has a passion for his work and teaching others Ruby.

Without further ado, let’s rock this:

Chapter 10: Construct Your Classes from Short, Focused Methods

  • Three characteristics in a good method, using the composed method technique (loc. 2101):
    • Each method should do a single thing, focusing on solving a single aspect of the problem.
    • Each method should operate on a single conceptual level. A method that is sorting stocks based on their type should not concern itself with its current price.
    • Each method should have a name that reflects its purpose. The name should help guide you through the code’s logic.
  • If you have a lot of if/else statements, the code may get hard to follow or just too long. Consider a construction more like this (loc. 2141):
    def life_goals
      return :become_a_rock_god if become_a_rock_god?
      return :build_satellites  if build_satellites?
      return :become_a_ruby_dev if become_a_ruby_dev?
      return :world_domination  if world_domination?
    end
    
    def become_a_rock_god?
      age < 17 && naivete > 1000
    end
    
    # ...
    
    # true story
  • If I need inspiration for writing good composed methods, investigate ActiveRecord::Base more. (loc. 2158)

Chapter 11: Define Operators Respectfully

  • Defining a binary operator, like +, -, /, *, %, &, |, ^, is translated into a method call on an object. (loc. 2191)
    x = y + z

    Is the same as:

    x = y.+(z)      # + is the method call, y is the object
  • You can also redefine unary operaters. + and – unary operators have to be defined a little differently, using +@ and -@in the method definition. (loc 2240)
    def +@
      # code goes here
    end
  • If you define bracket methods in an object, define a size method too. Users need to be able to tell when they’re running out of array. (loc. 2253)
  • Defining a binary operator method for an object a that knows what to do with another type of object b does not mean that object b knows what do with object a. (loc. 2274) In code:
    class ClassOfA
      def +(other_object_b)
        # code that works with object b
      end
    end
    
    a = ClassOfA.new
    b = ClassOfB.new
    a + b             # works!
    b + a             # doesn't work unless we define that method in ClassOfB

Chapter 12: Create Classes That Understand Equality

  • Ruby’s Object class defines four equality methods, equal?, =====, eql?. (loc. 2387)
  • equal? only returns true iff the two objects reference the same object id.
    foo = "ruby"
    bar = "ruby"
    foo.equal?(bar)
    => false
    
    foo = :ruby
    bar = :ruby
    foo.equal?(bar)
    => true

    This is already handled so well by Ruby, that equal? should not be overridden. (loc. 2398)

  • At the Object level, == does the same thing as equal?. Its descendants however, like String or Fixnum, change this behavior to check against things like values. The take home is feel free to override it. (loc. 2409)
  • The instance_of? method can be used to check that an object is the same class as the receiving object, no subclasses allowed. kind_of? can be used to check for classes or subclasses. This is unidirectional. (loc. 2425)
    a = String.new
    b = Object.new
    
    a.instance_of?(b.class)
    => false
    
    a.kind_of?(b.class)
    => true
    b.kind_of?(a.class)
    => false
  • The respond_to? method asks the object if it has the argument as a method. The method being checked takes the form of a symbol. (loc. 2438)
  • The main use for === is in case statements. By default, it’s equivalent to == but sometimes it’s overridden by descendents. For instance, in case statements when Ruby encounters a string, it behaves more like =~. Unless you specifically override ===, wherever you send ==, === will follow. It’s a good idea to leave === alone unless doing so results in ugly case statements. (loc. 2505)
  • The eql? method is used by the Hash class to determine if two keys are the same key. In the Object class, eql? returns true iff the object has the same id (it acts like equal?). (loc. 2545)
  • As long as you follow the general understand that if a.eql?(b) then a.hash == b.hash, feel free to implement your own version of the hash and eql? method. (loc. 2550)
  • The exclusive or operator ^ is a simple way of mishmashing two numbers, which is what we can use to redefine the method hash for our class. (loc. 2558)
    def hash
      some_object.hash ^ other_object.hash
    end

Chapter 13: Get the Behavior You Need with Singleton and Class Methods

  • Use singleton methods to build an object whose behavior is not completely controlled by its class. (loc. 2620)
  • Two ways to defining a singleton method for an instance (loc. 2643):
    humble_rubyist = Rubyist.new(name: "Phil Aquilina", personality:
    "Persistent little bugger")
    
    def humble_rubyist.ever_learning?
      true
    end

    If you want to define multiple methods, it might be better to use this technique (loc. 2652):

    class << humble_rubyist
      def wrong_about_something?
        # code for coin flip
        # just kidding, future employers
      end
    
      def drinking_coffee?(time)
        # eh, pretty much a coin flip again
      end
    end
  • The singleton class is an actual hidden class. It sits between every object and its regular class and until you add something, is a methodless shell. You might also hear them referred to as metaclasses or eigenclasses. (loc. 2657)
  • Singleton methods are actually at work when you build class methods (loc. 2674). To see this in action, check this out:
    humble_rubyist.class
    => Rubyist              # der
    Rubyist.class
    => Class                # Andddd boom goes the dynamite

    My Rubyist class is an instance of Class, just like humble_rubyist was an instance of Rubyist. So I can use the same techniques above to define class methods.

    class Rubyist
      def self.finger_dexterity_exercises
        # You see this technique all the time
        # It's equivalent to Rubyist.finger_dexterity_exercises
      end
    end

    Or:

    class Rubyist
      class << self
        def mad_coding_sesh
          # you limbered up first right?
        end
    
        def visit_hacker_news
          puts "dude, check this out!"
        end
      end
    end
    

    Cool right?
    (After looking at this code again, I realized these methods make pretty much no sense at a class method level. But for demo purposes, I think they serve.)

Chapter 14: Use Class Instance Variables

  • Class variables are dangerous if you don’t have a complete understanding of how they work. Do I have a complete understanding? No. Probably shouldn’t use them. Here’s how I understand what can go wrong (loc. 2784):
    1. We create a base class. No class variables made at this point.
      # guitar.rb
      
      class Guitar
        # ...
      end
    2. We create two subclasses, in two different files, with the same class variable inside.
      # strat.rb
      
      class Strat < Guitar
        @@default_user = :jimi
      end

      Different file:

      # gibson_sg.rb
      
      class GibsonSG < Guitar
        @@default_user = :angus
      end
    3. We’re ok here because Ruby, not finding @@default_user in Guitar, will attach the class variable to each subclass.
    4. But if we define @@default_user in Guitar, we’re gonna have a bad time. We’ve effectively created a bridge between the two subclasses.
    5. Now if you require the files in this order: require 'guitar', require 'strat', require 'gibson_sg', :angus is going to be set as default_user for all classes. This is generally unwanted behavior.
  • A better alternative to this is the class instance variable. To set a class instance variable, set an ordinary instance variable inside a class method. (loc. 2836)
    class Guitar
      def self.default_user
        @default_user = :someone
      end
    end

    Giving us Guitar.default_user to use without fear of it being something unexpected.

  • This is just a cool way of creating getter/setter class instance variables (loc. 2867):
    class Guitar
    
      @default_user = :someone
    
      class << self
        attr_accessor :default_user
      end
    end

Chapter 15: Use Modules as Name Spaces

  • A Ruby module is the container part of a class without the factory. You can’t instantiate a module, but you can put things inside a module. Modules can hold methods, constants, classes, and even other modules. (loc. 2925)
  • Advantages to wrapping classes in a module (loc. 2930):
    1. Allows you to group together related classes.
    2. Allows you to reduce the chances of a namespace collision.
  • Can access constants in the same way you access classes, ModuleName::CONSTANT_NAME. includeing the module means you can refer to the constant directly. (loc. 2930)
  • A rule of thumb for when to create a module: If you find yourself creating a lot of names that all start with the same word, then it’s time for a module. (loc. 2991)

Chapter 16: Use Modules as Mixins

  • Mixins allow you to share code without having to redo the inheritance tree. All you have to do is wrap the code in a module and include it in the class. The class will be able to use all of the module’s instance methods as its own instance methods. (loc. 3071)
  • If you want to make the methods in the module class methods, you are extendingthe module. (loc. 3074) The two ways to do this are:
    class YourClass
    
      class << self
        include ModuleOfInterest
      end
    end

    Or:

    class YourClass
      extend ModuleOfInterest       # Ruby, the sexiest language ever
    end
  • When you mix a module into a class, Ruby rewires the class heirarcy a bit, inserting the module as a psuedo superclass of the class. The module gets wedged betweent the class and its original superclass. (loc. 3089)
  • You can figure out if your object contains a given module with the kind_of? method. Or you can use the ancestors method on the class to see the complete inheritance ancestry. (loc. 3098)
    a = "string"
    a.kind_of?(Comparable)
    => true
    
    String.ancestors
    => [String, Comparable, Object, Kernel, BasicObject]
  • So how does Ruby lookup the inheritance tree when we include multiple modules?
    class CheeseSteak < Sandwich
      include Ingredients
      include ExoticIngredients
    
      # ...
    end

    The last include statement becomes the nearest parent class of the CheeseSteak class, with Ingredients next, and Sandwich after that. (loc 3124)

Chapter 17: Use Blocks to Iterate

  • Inside a method, you can detect whether your caller has passed a block using the block_given? method, and can fire off the block using yield. (loc. 3185)
    def meep_meep
      yield(argument_for_block) if block_given?
    end
  • Blocks always return a value – the last expression that the block executes - which your yielding method can either use or ignore. (loc 3191)
    def meep_meep
      returned_value = yield(argument_for_block) if block_given?
      puts "I am returning #{returned_value}"
    end
  • An aspect of iterators that beginners often overlook is that you can write iterators that run through collections that don’t actually exist. An example (loc. 3221):
    12.times { |x| puts "The number is #{x}" }
  • How to use the Enumerable module. First, make sure your class has an each method. Second, include Enumerable. This adds a ton of collection-related methods that rely on the each method. Some examples are include?, to_a, find, find_all. (loc. 3241) More details on this awesome tool here.
  • You can also create an Enumeratorinstance, passing in the collection and the name of the iterating method, receiving an object that knows how to sequence through your collection using that method. (loc. 3268)
    my_object = MyObject.new
    my_enum_object = Enumerator.new(my_object, :iterating_method)
    enum_my_object.count
    enum_my_object.sort

    This seems really useful and warrants more practice from me.

  • What if you’ve opened an expensive resource, you’re iterating through it, and it raises an exception? That resource never gets closed. Woops. You can use ensure in your method to try to guarantee something happens if an exception gets raised. (loc. 3293)
    begin
      while something_happening
        yield(expensive_resource.item)
      end
    ensure
      expensive_resource.close
    end
  • When called from inside a block, break will trigger a return out of the method that called the block. return inside a block will cause the method that you are defining to return. Both break and return will trigger any surrounding ensure clauses. (loc. 3298)

Chapter 18: Execute Around with a Block

  • Execute aroundis a technique used to bury the details in a method that takes a block. Use it when you have something, like logging, that needs to happen before or after some operation, or when the operation fails with an exception. (loc. 3368) Pretty cool example from the chapter:
    def with_logging(description)
      begin
        @logger.debug( "Starting #{description)" }
        yield
        @logger.debug( "Completed #{description)" }
      rescue
        @logger.error( "#{description} failed!" }
        raise
      end
    end
  • Execute around can also get objects initialized. (loc. 3381)
    class Phile
      attr_accessor :phile_path, :title
    
      def initialize(phile_path, title = '')
        @phile_path = phile_path
        @title = title
        yield(self) if block_given?
      end
    
      # ...
    end
    
    file = Phile.new("path/to/philly") do |f|
      f.title = "The Hills Are Alive! Runnnn!"
      f.do_stuff
      f.close
    end
  • All of the variables that are visible just before the opening do or { are still visible inside the code block. Code blocks drag scope in which they were created wherever they go. (loc. 3394)
  • A critical difference between just using execute around and really applying it elegantly lies in the name you pick for your method. A good name should make sense in the context of the application code, the code that is calling the method. Don’t think of it so much as naming a new method as naming a new feature that you are adding to the Ruby language. (loc. 3418)

Chapter 19: Save Blocks to Execute Later

  • If you add a parameter prefixed by an ampersand to the end of your parameter list, Ruby will turn any block passed into a method into a garden-variety parameter. After you have captured the block explicitly, you can run it with the callmethod. (loc. 3301)
      def your_wish_is(&my_command)
        puts "My command:"
        my_command.call
      end
  • Explicit code block advantages (loc. 3477):
    • Makes it easy to determine at a glance which methods expect a code block.
    • Methods can treat the block as an object.
    • You can hold onto the block and store a reference to it like any other
      object, which means you can execute the block later.
  • We can do use blocks for lazy initialization too. Using something like this allows us to be flexible in how we accept content (loc 3518):
    class ContentAggregator
      def initialize(uno, dos, &block)
        @uno = uno
        @dos = dos
        @initializer_block = block
      end
    
      def content_capture
        if @initializer_block
          @content = @initializer_block.call
          @initializer_block = nil
        end
        @content
      end
    end
    

    Allowing us to do something like this:

      file = ContentAggregator.new('uno','dos') do
        File.read('file.txt')
      end

    Or:

      my_site = ContentAggregator.new('uno','dos') do
        Net::HTTP.get_response('www.philaquilina.com', '/').body
      end
  • Differences in Proc.new and lambda (loc 3544):
    • Proc.new allows for the incorrect number of arguments to pass to its call method. The call method on an object returned by lambda will throw an exception if this is the case.
    • If Proc.new executes an explicit return, Ruby will return not just from the block but from the method that called the block. Same interesting behavior with break and next. The Proc object returned from lambda will simply return from the block and no further.
  • If you want a block object that behaves more like the one Ruby generates when you pass braces into a method, use Proc.new. If you want something that will behave more like a regular object with a single method, use lambda.
  • Finally, because blocks drag variables into their scope, be careful what is left around when the block begins. An expensive and unneeded resource outside the block becomes an expensive and unneeded resource inside the block. Set that thing to nil before the block begins. (loc. 3566)

And those are all my notes from Chapters 10 – 19. Next week, the final part of Eloquent Ruby, which is about metaprogramming (dun dun dunnnnn). If you have any awesome insights or just some corrections, feel free to throw them down in the comments section. Thanks for reading.