DEV Community

Brandon Weaver
Brandon Weaver

Posted on

Let's Read – Eloquent Ruby – Ch 7

Perhaps my personal favorite recommendation for learning to program Ruby like a Rubyist, Eloquent Ruby is a book I recommend frequently to this day. That said, it was released in 2011 and things have changed a bit since then.

This series will focus on reading over Eloquent Ruby, noting things that may have changed or been updated since 2011 (around Ruby 1.9.2) to today (2024 — Ruby 3.3.x).

Chapter 7. Treat Everything Like an Object—Because Everything Is

Something that is easy to forget, given all the talk of every different paradigm Ruby supports, is that at its core it's a deeply Object Oriented language. This chapter explores a lot of those themes, along with the common tools Ruby uses across all objects.

A Quick Review of Classes, Instances, and Methods

Russ starts in with the Document class mentioned in the first chapter:

class Document
  # Most of the class omitted...

  # A method
  def words
    @content.split
  end

  # And another one
  def word_count
    words.size
  end
end
Enter fullscreen mode Exit fullscreen mode

...and how we make a new instance of that class:

doc = Document.new( 'Ethics', 'Spinoza', 'By that which is...' )
Enter fullscreen mode Exit fullscreen mode

As a refresher you might see the syntax ClassName.method_name for class methods and ClassName#method_name for instance methods. This originated in Smalltalk and was carried over to a lot of Ruby's documentation, and it's good to know what it means.

I bring this up to say you might see Document#word_count to represent what would happen if we called doc.word_count on the instance of a Document we've just created in the above example. Later chapters will get more into it, but knowing how those two tend to be annotated will be extremely useful even at this point in the book if you were to go off and read other Ruby code.

On self

Russ gives an example of what self is in the following code:

class Document
  # Most of the class on holiday...

  def about_me
    puts "I am #{self}"
    puts "My title is #{self.title}"
    puts "I have #{self.word_count}"
  end
end

doc = Document.new('Ethics', 'Spinoza', 'By that which is...')
doc.about_me
# STDOUT: I am #<Document:0x1234567>
# STDOUT: My title is Ethics
# STDOUT: I have 4 words
Enter fullscreen mode Exit fullscreen mode

self represents, in this case, the current instance which would be doc . There are ways to manipulate this and change that, but at this point in the book we can reasonably assume such until later on.

One thing he does here is show the usage of self in the explicit sense in front of title as self.title and word_count as self.word_count. You can omit both, but it does serve as an example that self is implied in these cases. There are some further complexities with namespace scoping and variables vs method calls, but that will also come up later in the book.

Russ does mention this though:

Don’t write self.word_count when a plain word_count will do.

Superclasses

Russ mentions in the book that every class, except one, has a superclass. The book doesn't mention it, but that class is BasicObject. Try it yourself:

class Test; end

Test.superclass # Object
Object.superclass # BasicObject
BasicObject.superclass # nil
Enter fullscreen mode Exit fullscreen mode

In that you'll notice that our quick Test class inherits from Object as its superclass. If we don't specify a class our objects inherit from Object by default. Russ uses the following example of explicitly defining a superclass here:

# RomanceNovel is a subclass of Document, which is a subclass of Object
class RomanceNovel < Document
end
Enter fullscreen mode Exit fullscreen mode

He then goes on to mention that when trying to find a method Ruby will go through every superclass until it finds it, like so:

class Grandparent
  def one = 1
end

class Parent < Grandparent; end
class Child < Parent; end

Child.new.one
# => 1
Enter fullscreen mode Exit fullscreen mode

For now this is mostly correct, but later we'll get into method_missing which will expand upon this a good deal. The basic intuition here will still be handy to know.

Objects All the Way Down

The book then gets into how Ruby's OO philosophy shapes the ways methods work. The first example is Numeric#abs which can be used to get an absolute value:

-3.abs
# => 3
Enter fullscreen mode Exit fullscreen mode

The book mentions in other languages you might have abs(-3) instead, but for Ruby the method belongs to the object it's being called on, and in fact it belonging to that object is what gives it enough information to work.

The book then gets into a few more examples:

# Call some methods on some objects

"abc".upcase
# => "ABC"
:abc.length
# => 3
/abc/.class
# => Regexp
Enter fullscreen mode Exit fullscreen mode

...just to drive the point that pretty much everything in Ruby is an object.

Wait, Pretty Much?

Yes, sneaky words those. If I had to clarify I would say that everything that can have a value is an object whereas keywords and control flows are not like if and case. In Smalltalk some of those are still object methods, but in Ruby they're expressions and keywords.

There are some other gray areas like blocks, and entire threads of arguments on the subject, but the general statement that pretty much everything is an object is true enough to still hold a lot of value in your intuition of Ruby.

Russ even gets to this in the next section with an equally apt description:

In Ruby, if you can reference it with a variable, it’s an object.

...though some pedants might still go on about blocks being assigned to variables.

Back on Topic

The book continues on into true and false, which surprise, are also objects:

true.class
# => TrueClass

false.class
# => FalseClass
Enter fullscreen mode Exit fullscreen mode

But what might be surprising here is there's no Boolean class to wrap the two. On one hand they're both very similar in that they represent predicate states, but on the other they're the exact opposite of each other. To quote Matz, the creator of Ruby:

...There's nothing true and false commonly share, thus no Boolean class. Besides that, in Ruby, everything behave as Boolean value....

Which makes sense given the implicit nature of a lot of Ruby you might read:

document.do_something if document
Enter fullscreen mode Exit fullscreen mode

...in which we do something with a document if it's neither nil nor false. One thing to keep in mind with Ruby is that it heavily uses implicit expressions, and Rails does even more so. That doesn't always make it correct or necessarily the most understandable, but once you know what and why Ruby is doing something it allows you to skip a lot of boilerplate code.

The danger here, of course, is it's the "Rest of the Owl" meme for some folks so personally I prefer to edge a bit more explicit even if not strictly necessary to make sure someone with even a few days of Ruby experience can reasonably understand what I'm up to with a particular piece of code.

The Importance of Being an Object

Going back to Russ's quote here:

In Ruby, if you can reference it with a variable, it’s an object.

He continues on to say that if this holds you can also probably assume it's an instance of Object somewhere down the tree as well, meaning they all share a common core of methods which are very useful to familiarize yourself with. In fact I would advocate for reading the Object docs directly at least a few times.

Going through some of the methods the book mentions we get methods like class and instance_of? from Object, and a lot of default implementations of things like to_s (to string). For to_s the default implementation will return back the object and its object id:

doc = Document.new('Emma', 'Austin', 'Emma Woodhouse, ...')
puts doc
# STDOUT: #<Document:0x1234567>
Enter fullscreen mode Exit fullscreen mode

...which is the same thing we saw from earlier with outputting self above. That also means we can write our own to_s method, as mentioned in the book:

class Document
  def to_s
   "Document: #{title} by #{author}" 
  end
end

doc = Document.new('Emma', 'Austin', 'Emma Woodhouse, ...')
puts doc
# STDOUT: Ethics by Spinoza
Enter fullscreen mode Exit fullscreen mode

He also briefly touches on eval to create our very own IRB-like REPL using nothing but a few simple methods from Object:

while true
  print "Cmd> "
  cmd = gets
  puts eval(cmd)
end
Enter fullscreen mode Exit fullscreen mode

Note: Eval is very powerful, as the book mentions, but also very dangerous. Never use it in contexts where raw user input can get into it, lest some clever individual manages to send your Ruby program some variant of system("rm -rf /").

Reflections

The book then gets into some of the reflection methods Ruby supplies to introspect into an object, like:

doc.public_methods
# => (all methods of the object, including parents)

doc.public_methods(false)
# => (only methods defined in the Document class)

doc.instance_variables
# => [:@title. :@author, :@content]
Enter fullscreen mode Exit fullscreen mode

Now there are some variants here such as methods, instance_methods, and others which is why reading through the Object docs can be handy. Often times I find if a method sounds reasonable Ruby probably has it.

Public, Private, and Protected

The book then goes into some visibility controls, namely:

  • public - Available to everyone
  • protected - Available only to this class and sub-classes
  • private - Available to only this class

...and provides the following two methods of making something private:

class Document
  private # Methods below here are private

  def word_count
    words.size
  end
end

class Document
  def word_count
    words.size
  end

  private :word_count # Only one method made private
end
Enter fullscreen mode Exit fullscreen mode

...but the second case is more modernly written as:

private def word_count
  words.size
end
Enter fullscreen mode Exit fullscreen mode

Why? Because defining a method returns its name as a Symbol:

def testing = 1
# => :testing
Enter fullscreen mode Exit fullscreen mode

In general with privacy you should expose as little information as necessary as public methods. Focus on a clear, minimal interface that cleanly encapsulates the data in the class otherwise you end up with everyone reaching into every facet of every Object. While the book mentions that these access controls are not common in Ruby core I would personally encourage liberal use of them as untangling object dependencies down the road in a 1M+ lines of code app becomes a daunting task.

Back to the book it then goes into what happens when we call private methods. Simply put they raise exceptions:

class PrivateThings
  private def private_method = 1
end
# => :private_method

PrivateThings.new.private_method
(irb):25:in `<main>': private method `private_method' called for #<PrivateThings:0x000000010692b128> (NoMethodError)

PrivateThings.new.private_method
                 ^^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

...but it's also mentioned that you could get around this with send:

PrivateThings.new.send(:private_method)
# => 1
Enter fullscreen mode Exit fullscreen mode

In general you should avoid using send unless you have no other options, as things are generally private for a reason. That includes in your tests, because private methods should be exercised and tested via public interfaces instead.

In the Wild

The book mentions, rightly so, that so much of Ruby is about methods and how they're called. If everything is an Object it stands to reason the actions you can take upon them are also rather important. It also mentions that most everything, even some surprising things, are methods. Even most operators are:

1.+(2) == 1 + 2
# => 2
Enter fullscreen mode Exit fullscreen mode

The things that aren't are things like control flow (if, unless, case, etc), assignments (a = 1), loops (while, loop, until), and more. While not a comprehensive list the full one isn't much larger than that.

Even require, used for loading other code, is a method.

The book then mentions the attr_* methods as another example:

class Person
  # All of these are methods
  attr_accessor :salary
  attr_reader :name
  attr_writer :password
end
Enter fullscreen mode Exit fullscreen mode

...and by chapter 26 the book will go over implementing your own.

Staying Out of Trouble

Remember earlier when public_methods was said to return some 50+ methods? That means there's a lot of room to accidentally use the same name like in the example the book provides:

class Document
  # Send this document via email
  def send(recipient)
    # Do some interesting SMTP stuff
  end
end
Enter fullscreen mode Exit fullscreen mode

Another example they provide of things going wrong are if you have an error in your to_s method:

class Document
  def to_s
    "#{title} by #{aothor}" # oops!
  end
end
Enter fullscreen mode Exit fullscreen mode

...which will ensure you can't use puts for your documents, though I would argue the exceptions will still be informative here.

The last is mentioning how often there are multiple ways to do something in Ruby and sometimes they all converge into a much simpler solution if you know how the language works:

if the_object.nil?
  puts "The object is nil"
elsif the_object.instance_of?(Numeric)
  puts "The object is a number"
else
  puts "The object is an instance of #{the_object.class}"
end
Enter fullscreen mode Exit fullscreen mode

...when it could be written more simply as:

puts "The object is an instance of #{the_object.class}"
Enter fullscreen mode Exit fullscreen mode

There's a lot of power in a unified interface, and hey, it's why Interface is such a popular term in programming in general. With Ruby there are more than a few of these "interfaces", normally suffixed with able like Enumerable, Comparable, and others. Learning them, and how to leverage them, can be very powerful.

Wrapping Up

The book closes this chapter by reiterating the main point that almost everything in Ruby is an Object. I would add that unified interfaces are also exceptionally good to consider in Ruby, as a lot of code can be dramatically simplified when one knows of them.

While privacy was a narrower section of this chapter I would heavily encourage making as few methods public as possible on any class you write. Reduce its public interface as much as possible, and in doing so you make the boundaries of your program much clearer. If we made everything public then anyone can do anything with any data anywhere, and take it from me when I say that can get messy very quickly, especially at scale.

Top comments (0)