According to irb,
>> StringIO.is_a?(IO)
>> => false
This seems illogical to me. Is this intentional? If so, why?
One answer to this:
Since StringIO doesn't use IO to base its implementation, it is not an IO.The original question (and some posts in the mailing list debate) seems to come from the confusion about the role of inheritance in OOP. StringIO and IO don't need to go back to the same superclass, because they don't share any commonalities except one: a set of methods they support.
Why does it matter? If you use standard duck typing, it shouldn't. Class hierarchy doesn't matter with duck typing. Classes are just an implementation detail with duck typing. You are best off not looking #is_a?, #respond_to?, etc. Just assume the object can do the methods you'll be using and use.
This sounds like the basic idea of duck typing: instead of requiring an object to be of a certain type, it suffices that the object responds to a set of methods.
None of this is new or specific to Ruby. By replacing the word "methods" with the OOP term "messages", the above statement reads: "[...] it suffices that the object responds to a set of messages". This does sound a lot like the idea of a (network) protocol. After all, a system that understands and responds to messages such as "PUT", "GET", "POST", etc. sent to it in a certain manner, can be said to understand the HTTP protocol.
Let's go further with this idea: a client, say a web browser, only cares that a server it communicates with understands HTTP; it does not require the server to be of a certain type or make. After all: the web would have had a harder time growing, if, say, the Mosaic browser had been hardcoded to require the server on the other end of the line to be NSCA's or Netscape's HTTP servers. By ignoring the other end of the communication, and only requiring that a "GET" is answered in a certain way, both ends of the webs development (client, server) were able to evolve independently.
The similarity of this approach to protocols was clear to users of OOP languages long ago. Smalltalk and ObjectiveC, both dynamic OOP languages, have long used the term Protocol to refer to this concept.
The Protocol concept is certainly useful, if only to give a name to a particular set of messages. This also helps with clearing up the questions raised in the above mailing list post. What the StringIO and IO share is not common ancestry but a common Protocol.
The concept is certainly not limited to dynamic languages. On the more static side, Java made a crucial step by introducing
interfaces
, as a way of:
- naming a set of messages
- statically checking that a class actually implements them
Of course, as is often the case with static guarantees, interfaces only check the presence of a set of methods with certain signatures - it doesn't guarantee the behavior or even the returned values of the methods. It's not even guaranteed that the method is invokable - it's still prudent to watch out for Java's
java.lang.UnsupportedOperationException
when working with, say, Java's Collection API. Interfaces do have their own share of problems, such as evolvability. Changes to an interface are breaking changes for all classes implementing it. Big projects, such as Eclipse, have guidelines for API evolution, which rule that an interface can't be changed once it's been published. This sometimes yields the advice to favor Abstract classes over interfaces, since Abstract classes can add new functions, but define empty implementations, so subclasses can, but don't have to implement them. This problem could, of course, be solved by making fine grained interfaces, with one method each, and building the full interface up with new interfaces that extend the previous ones, each adding a single API call.
The problem of ensuring that an object supports a Protocol could of course be solved in Ruby by delegating the interface check to a predicate function. An example:
def supports_api?(obj)While this works, it's also fraught with subtle problems -
obj.respond_to?(:length) && obj.respond_to?(:at)
end
respond_to?
only works with methods defined in a class. If an object implements parts of its interface with method_missing
(e.g. a Proxy), respond_to?
would return false to these methods, although a call to these methods would work. This means that respond_to?
checking can work, but it's not a drop-in replacement for static checks (Rick de Natale refers to this coding style as "Chicken Typing", i.e. it's a kind of defensive programming).Another, static, approach to fine grained interfaces are Scala's Structural Types:
def setElementText(element : {def setText(text : String)}, text : String) =This function requires the argument
{
element.setText(text.trim()
.replaceAll("\n","")
.replaceAll("\t",""))
}
element
to have a setText(text:String)
method, but does not care about the particular type of the object. The focus on specific types or class hierachies of an object also limits the flexibility. An interface that requires an
int
parameter can only be called with an int or - through AutoBoxing - an Integer, but nothing else. A method that just requires the object to have an to_i method (that returns an integer number, either Fixnum or BigNum) is more flexible in the long run. Classes not under the developers control might not inherit from the needed classes, although they might still support a particular Protocol. Somewhere between the dynamic (duck typing) and static (interfaces, Scala's Structural Types) are Erlang's Behaviors. Erlang's modules (which are comparable to Ruby's Modules) group functions together. A module can use
-behaviour(gen_server).
to state that it implements the Behavior gen_server
- which means that it exports a certain set of functions. This is supported by the Erlang compiler in that it complains if the module doesn't export all of the Behaviors required functions. This also shows that the general principle of Protocols is not limited to languages that define themselves as "OOP" languages by providing concepts of Class, Object, Inheritance, etc.
How do you document the use of Protocols in Ruby, i.e. how do you talk about the set of messages that interacting objects need to agree upon?