Sunday, March 2, 2008

Quacking Again - More On Duck Typing

I hadn't intended to write about Ruby again so soon, but my last post stirred up some controversy. So, I thought I'd present some better examples and try to accurately represent all of the positions. (Don't worry, I'll get back to criticizing C#, JavaScript and other languages shortly.)

Some people thought that my example was a "strawman". I disagree. If you have a set of objects and you don't know their types, you might not know their capabilities. Isn't that part of the point of dynamic typing? I frequently implement to_html in my classes and I frequently implement methods like name, long_name, full_name, etc. It's helpful if code which uses methods like these can fall back to other methods. Saying "you should just implement to_html everywhere" is not an acceptable answer. I'm busy trying to render a web page -- I don't want to have to go through the rest of my system, including classes I didn't write, and implement to_html everywhere just in case I happen to get a particular type of object.

Some people thought I was saying interface declarations should be required. No, no, no. I'm just opposed to discarding information that would help me write better code.

Some people argued that Ruby doesn't have a compiler. Why should I care? That's an implementation detail that should be irrelevant to anyone using the language. C# could be interpreted and Ruby could be compiled.

Do interfaces allow for compile-time checking or runtime checking? The answer is, to some extent, both. But my point was not about exceptions -- it was about writing better code. The key points are:

  • Don't discard useful information known by the developer because there's no way to specify it in the language. At a minimum, comment it (much Ruby code, and code in general), doesn't do this). But, even better, formalize it.
  • When an exception does occur, raise it at the highest level possible. The deeper an exception is raised, the harder it is to figure out what the problem is. Avoid the irony of a deep exception for a condition known by the developer when they first wrote the code.
  • Let the compiler and runtime do as much work for the programmer as possible. This is DRYness at its best.
Do you disagree with any of those points?

In the examples below, I have tried to write each example in the best way possible. All have the same functionality, except as noted. Although I know Ruby fans usually omit parens after method calls that don't take arguments, I've included them in the interests of clarity (and I prefer them myself). If you have a way to improve one of the examples, let me know or add a comment, and I'll update the example.

You can decide which way you prefer. I know which one I'd like to see.

Example 1: Using respond_to?
def render1(obj)
case
when obj.respond_to?(:to_html)
return obj.to_html()
when obj.respond_to(:to_json)
return json_to_html(obj.to_json())
when obj.respond_to(:to_s)
return html_encode(obj.to_s())
end
end
Example 2: Using rescue NoMethodException
This example does not work properly if any other NoMethodException occurs within the called methods. Any such errors would be masked.
def render2(obj)
return obj.to_html()

rescue NoMethodException => e
begin
return json_to_html(obj.to_json())

rescue NoMethodException => e
return html_encode(obj.to_s())
end
end
Example 3: Using rescue NoMethodException and checking the Exception message
This example does not work properly if any other NoMethodException for the same method occurs within the called methods (which is a possibility if to_html calls to_html on contained objects). Any such errors would be masked.
def render3(obj)
return obj.to_html()

rescue NoMethodException => e

# Warning: assumes particular format of Exception message
raise if (e.message !~ /method `to_html'/)

begin
return json_to_html(obj.to_json())

rescue NoMethodException => e
# Warning: assumes particular format of Exception message
raise if (e.message !~ /method `to_json'/)

return html_encode(obj.to_s())
end
end
Example 4: Using a common send_in_order method
Also fixes limitation of example 3. Would break if Object.send gets overridden.
# send_in_order sends a series of methods, in order, to an object.
# Returns the method that was successful and the result of the method call
# Known limitation: All of the methods must take the same parameters
# Could be modified to handle that, but not necessary for this example.
class Object
def send_in_order(methods, *params)
methods.each { |m|
begin
result = self.send(m, *params)
return m, result
rescue NoMethodError => e
# Warning: assumes particular format of Exception message and backtrace
if (e.message !~ Regexp.new("method `" + m.to_s() + "'") ||
e.backtrace[0] !~ /in `send'/ ||
e.backtrace[1] !~ /in `send_in_order'/)
raise
end
end
}
end
end
def render4(obj)
method_called,result = obj.send_in_order([:to_html, :to_json, :to_s])

case method_called
when :to_html:
return result
when :to_json:
return json_to_html(result)
when :to_s:
return html_encode(result)
end
end

Example 5: Using interfaces (with made up syntax)
Yes, the syntax below for defining interfaces isn't even close to fully thought out.
interface IConvertsToHtml
supports to_html()
end
interface IConvertsToJson
supports to_json()
end
def render5(IConvertsToHtml obj)
return obj.to_html()
end

def render5(IConvertsToJson obj) < (IConvertsToHtml obj)
return json_to_html(obj.to_json())
end

def render5(arg) # Always lowest priority
return html_encode(obj.to_s())
end

2 comments:

tante said...

I was one of the people that thought that your argument was a straw man and I think that this is pretty much just a repetition of it, let me make this clearer.

Interfaces allow you to control the environment that a function can be called in: You can specify which type something has. That is based on the idea that you have a set of classes already defined that model your whole area of interest.

In dynamic languages you (well "I") don't think like that: You know that you will not have "controlled environments" so (because "it's easier to ask for forgiveness than for permission") you just do what you want to do and handle the errors. That's not lazy, that's the way those languages work and that's the idiomatic way to use them.

You can use constructs like "assert" (Ruby probably has something similar) to control the environment but that would bloat the code and make it harder to understand so it should be avoided.

Raise/Throw meaningful exceptions that tell you what is wrong. When writing a function write in the docstring/comment for it, what "things" it can handle, use tests to make sure it all works together.

So I do agree somewhat: You want some documentation about what some function expects, but putting it in the code with interfaces (or the workarounds you already presented) is very unidiomatic for dynamic languages and should be avoided.

Andrew M Greene said...

I agree with Roy's basic point, but I think the example is poorly chosen. Whether to render an object as HTML or a JSON object is not a choice the object should be making, it's a choice that the high-level code should be making based on the context in which the object is being serialized.

Post a Comment