Eloquent Ruby: Embrace Dynamic Typing
- Shorter Programs, But Not the Way You Think
- Extreme Decoupling
- Required Ceremony Versus Programmer-Driven Clarity
- Staying Out of Trouble
- In the Wild
- Wrapping Up
How? Why? These are the two questions that every new Ruby coder—or at least those emigrating from the more traditional programming languages—eventually gets around to asking. How can you possibly write reliable programs without some kind of static type checking? And why? Why would you even want to try? Figure out the answer to those two questions and you're on your way to becoming a seasoned Ruby programmer. In this chapter we will look at how dynamic typing allows you to build programs that are simultaneously compact, flexible, and readable. Unfortunately, nothing comes for free, so we will also look at the downsides of dynamic typing and at how the wise Ruby programmer works hard to make sure the good outweighs the bad.
This is a lot for one chapter, so let's get started.
Shorter Programs, But Not the Way You Think
One of the oft-repeated advantages of dynamic typing is that it allows you to write more compact code. For example, our Document class would certainly be longer if we needed to state—and possibly repeat here and there—that @author, @title, and @content are all strings and that the words method returns an array. What is not quite so obvious is that the simple "every declaration you leave out is one bit less code" is just the down payment on the code you save with dynamic typing. Much more significant savings comes from the classes, modules, and methods that you never write at all.
To see what I mean, let's imagine that one of your users has a large number of documents stored in files. This user would like to have a class that looks just like a Document,1 but that will delay reading the contents of the file until the last possible moment: In short, the user wants a lazy document. You think about this new requirement for a bit and come up with the following: First you build an abstract class that will serve as the superclass for both the regular and lazy flavors of documents:
class BaseDocument def title raise "Not Implemented" end def title= raise "Not Implemented" end def author raise "Not Implemented" end def author= raise "Not Implemented" end def content raise "Not Implemented" end # And so on for the content= # words and word_count methods... end
Then you recast Document as a subclass of BaseDocument:
class Document < BaseDocument attr_accessor :title, :author, :content def initialize( title, author, content ) @title = title @author = author @content = content end def words @content.split end def word_count words.size end end
Finally, you write the LazyDocument class, which is also a subclass of BaseDocument:
class LazyDocument < BaseDocument attr_writer :title, :author, :content def initialize( path ) @path = path @document_read = false end def read_document return if @document_read File.open( @path ) do | f | @title = f.readline.chomp @author = f.readline.chomp @content = f.read end @document_read = true end def title read_document @title end def title=( new_title ) read_document @title = new_title end # And so on... end
The LazyDocument class is a typical example of the "leave it to the last minute" technique: It looks like a regular document but doesn't really read anything from the file until it absolutely has to. To keep things simple, LazyDocument just assumes that its file will contain the title and author of the document on the first couple of lines, followed by the actual text of the document.
With the classes above, you can now do nice, polymorphic things with instances of Document and LazyDocument. For example, if you have a reference to one or the other kind of document and are not sure which:
doc = get_some_kind_of_document
You can still call all of the usual document methods:
puts "Title: #{doc.title}" puts "Author: #{doc.author}" puts "Content: #{doc.content}"
In a technical sense, this combination of BaseDocument, Document, and LazyDocument do work. They fail, however, as good Ruby coding. The problem isn't with the LazyDocument class or the Document class. The problem lies with BaseDocument: It does nothing. Even worse, BaseDocument takes more than 30 lines to do nothing. BaseDocument only exists as a misguided effort to provide a common interface for the various flavors of documents. The effort is misguided because Ruby does not judge an object by its class hierarchy.
Take another look at the last code example: Nowhere do we say that the variable doc needs to be of any particular class. Instead of looking at an object's type to decide whether it is the correct object, Ruby simply assumes that if an object has the right methods, then it is the right kind of object. This philosophy, sometimes called duck typing,2 means that you can completely dispense with the BaseDocument class and redo the two document classes as a couple of completely independent propositions:
class Document # Body of the class unchanged... end class LazyDocument # Body of the class unchanged... end
Any code that used the old related versions of Document and LazyDocument will still work with the new unrelated classes. After all, both classes support the same set of methods and that's what counts.
There are two lessons you can take away from our BaseDocument excursion. The first is that the real compactness payoff of dynamic typing comes not from leaving out a few int and string declarations; it comes instead from all of the BaseDocument style abstract classes that you never write, from the interfaces that you never create, from the casts and derived types that are simply irrelevant. The second lesson is that the payoff is not automatic. If you continue to write static type style base classes, your code will continue to be much bulkier than it might be.