Type systems are a fundamental part of every programming language. In fact, the way a language designer approaches typing goes a long way towards outlining the way that thoughts are expressed in that language.
Statically typed languages like C++ and Java make us tend to think of objects as abstract data structures that fit within a neatly defined hierarchy. In these languages, there isn’t a major distinction between an object’s class and its type, as the two concepts are tied together directly at the implementation level. But the marriage of class and type found in these languages is not a universal law shared by all object oriented programming languages.
By contrast, Ruby’s dynamic nature facilitates a style of type system known as duck typing. In particular, duck typing breaks the strong association between an object’s class and its type by defining types based on what an object can do rather than what class it was born from. This subtle shift in semantics changes virtually everything about how you need to think about designing object oriented systems, making it a great topic for Practicing Ruby to cover.
While duck typing is possible in many other languages, Ruby is designed from the ground up to support this style of objected oriented design. In this issue, we will cover some of the options that are available to us for doing Ruby-style type checking.
Type Checking Techniques
There are three common ways to do type checking in Ruby, two of which involve duck typing, and one that does not. Here’s an example of the approach that does not involve duck typing.
def read_data(source) case source when String File.read(source) when IO source.read end end
If you’ve been working with Ruby for a while, you’ve probably written code that
did type checking in this fashion. Ruby’s case statement is powerful, and
makes this sort of logic easy to write. Our
read_data() function works as
expected in the following common scenarios:
filename = "foo.txt" read_data(filename) #=> reads the contents of foo.txt by calling # File.read() input = File.open("foo.txt") read_data(input) #=> reads the contents of foo.txt via # the passed in file handle
But things begin to fall apart a bit when we decide we’d like
work with a
Tempfile, or with a
StringIO object, or perhaps with a mock
object we’ve defined in our tests. We have baked into our logic the assumption that the input is always either a descendent of
String or a descendent of
IO. The purpose of duck typing is to remove these restrictions by focusing only on the messages that are being passed back and forth between objects rather than what class they belong to. The code below demonstrates one way you can do that.
def read_data(source) return source.read if source.respond_to?(:read) return File.read(source.to_str) if source.respond_to?(:to_str) raise ArgumentError end
With this modification, our method expects far less of its input. The passed in
object simply needs to implement either a meaningful
method. In addition to being backwards compatible with our non-duck-typed code,
this new approach gives us access to many useful standin objects, including:
Tempfile, mock objects for testing, and any user defined objects that are either IO-like or String-like but not a descendent of either.
However, the following contrived example illustrates a final corner case that calls for a bit of extreme duck typing to resolve. Try to spot the problem before reading about how to solve it.
class FileProxy def initialize(tempfile) @tempfile = tempfile end def method_missing(id, *args, &block) @tempfile.send(id, *args, &block) end end
This code implements a proxy which forwards all of its messages to the wrapped
tempfile object. However, like many hastily coded proxy objects in Ruby, it does not properly forward
respond_to?() calls to the object it wraps. The irb session below illustrates the resulting false negative in our test.
# Populate our tempfile through the proxy >> proxy = FileProxy.new(Tempfile.new("foo.txt")) => #<FileProxy:0x39461c @tempfile=#<File:/var/f..foo.txt.7910.3>> >> proxy << "foo bar baz" => #<File:/var/folders/sJ/sJo0IkPYFWCY3t5uH+gi0++++TQ/-Tmp-/foo.txt.7910.3> >> proxy.rewind => 0 # Unsuccessfully test for presence of read() method >> proxy.respond_to?(:read) => false # But read() works as expected! >> proxy.read => "foo bar baz"
This issue will cause
read_data() to raise an
ArgumentError when passed a
FileProxy. In this case, the best solution is to fix
respond_to?() so that it works as expected, but since you may often encounter libraries with bad behaviors like this, it’s worth knowing what the duck typing fundamentalist would do in this situation.
def read_data(source) begin return source.read rescue NoMethodError # do nothing, just catch the specific error you'd expect if # read() was not present. end begin File.read(source.to_str) rescue NoMethodError raise ArgumentError # now we've run out of valid cases, so let's # raise a meaningful error end end
With this final version, we preserve all the benefits of the previous duck
typing example, but we can work with objects that have dishonest
methods. Unfortunately, the cost for such flexibility includes code that is less
pleasant to read and is almost certainly going to run slower than either of our
previous implementations. Using the exception system for control flow isn’t cheap,
even if this is the most ‘pure’ form of type checking we can do.
While we’ve talked about the benefits and drawbacks of each of these approaches, I haven’t given any direct advice on whether one way of doing type checking is better than the others, simply because there is no simple answer to that question.
I will paint a clearer picture in the next article by showing several realistic examples of why duck typing can come in handy. Until then, I will leave you with a few things to think about.
Questions / Study Topics
Is explicit class checking ever absolutely necessary? Are their situations in which even if other options are available, checking the class of an object is still the best thing to do?
Name something weird that can happen when you write your contracts on the messages your objects respond to rather than what class of object they are.
Try to identify some feature of Ruby that relies on duck typing either for its basic functionality or as an extension point meant to be customized by application programmers.
Share a bit of code which does explicit class comparison that you think would be very difficult to convert to a duck-typing style.
Share a bit of code (either your own or from a OSS project you like) that you feel uses duck typing effectively.
Feel free to leave a comment below if any of the above topics interest you.
NOTE: This article has also been published on the Ruby Best Practices blog. There may be additional commentary over there worth taking a look at.
Practicing Ruby is a Practicing Developer project.
All articles on this website are independently published, open source, and advertising-free.