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::Basemore. (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
athat knows what to do with another type of objectbdoes not mean that objectbknows what do with objecta. (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
Objectclass 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
Objectlevel,==does the same thing asequal?. Its descendants however, likeStringorFixnum, 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 theHashclass to determine if two keys are the same key. In theObjectclass,eql?returns true iff the object has the same id (it acts likeequal?). (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 thehashandeql?method. (loc. 2550) - The exclusive or operator
^is a simple way of mishmashing two numbers, which is what we can use to redefine the methodhashfor 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
Rubyistclass is an instance ofClass, just likehumble_rubyistwas an instance ofRubyist. 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 endOr:
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 endCool 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):
- We create a base class. No class variables made at this point.
# guitar.rb class Guitar # ... end
- 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
- We’re ok here because Ruby, not finding
@@default_userinGuitar, will attach the class variable to each subclass. - But if we define
@@default_userinGuitar, we’re gonna have a bad time. We’ve effectively created a bridge between the two subclasses. - Now if you
requirethe files in this order:require 'guitar',require 'strat',require 'gibson_sg',:angusis going to be set asdefault_userfor all classes. This is generally unwanted behavior.
- We create a base class. No class variables made at this point.
- 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 endGiving us
Guitar.default_userto 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):
- Allows you to group together related classes.
- 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
includeit 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 endOr:
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 theancestorsmethod 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
CheeseSteakclass, withIngredientsnext, andSandwichafter 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 usingyield. (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
Enumerablemodule. First, make sure your class has aneachmethod. Second,include Enumerable. This adds a ton of collection-related methods that rely on theeachmethod. Some examples areinclude?,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
ensurein 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,
breakwill trigger a return out of the method that called the block.returninside a block will cause the method that you are defining to return. Bothbreakandreturnwill trigger any surroundingensureclauses. (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 endAllowing us to do something like this:
file = ContentAggregator.new('uno','dos') do File.read('file.txt') endOr:
my_site = ContentAggregator.new('uno','dos') do Net::HTTP.get_response('www.philaquilina.com', '/').body end - Differences in
Proc.newandlambda(loc 3544):Proc.newallows for the incorrect number of arguments to pass to itscallmethod. Thecallmethod on an object returned bylambdawill throw an exception if this is the case.- If
Proc.newexecutes an explicit return, Ruby will return not just from the block but from the method that called the block. Same interesting behavior withbreakandnext. TheProcobject returned fromlambdawill 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, uselambda. - 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
nilbefore 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.
