Friday, January 23, 2009

Shoes Class Hierarchy Redux: Introspection

After writing up the Shoes class hierarchy, someone pointed out that this is already included in the Shoes manual (and available online here. I took a look, and found some disagreements between that class hierarchy and the one I had come to. In particular, large numbers of classes are listed a being subclasses of Shoes::Basic, including some like EditBox and EditLine that are very definitely subclasses of Shoes::Native.

My first thought was that the manual must be just out of date, but upon looking into the source for the manual (located in the shoes source at lib/shoes/help.rb, as well as static/manual.txt), I discovered that the class hierarchy was actually being generated dynamically by introspecting the Shoes classes in the following method:


def class_tree
tree = {}
Shoes.constants.each do |c|
k = Shoes.const_get(c)
next unless k.respond_to? :superclass

c = "Shoes::#{c}"
if k.superclass == Object
tree[c] ||= []
else
k.ancestors[1..-1].each do |sk|
break if [Object, Kernel].include? sk
(tree[sk.name] ||= []) << c
c = sk.name
end
end
end
tree
end


This code takes the constants defined in Shoes, looks for those that have superclasses, and pulls out their ancestors up until Object. Why, then, if the manual is being generated dynamically, does it miss the fact that Shoes::EditLine, Shoes::EditBox, and a host of others inherit from Shoes::Native?

To debug this, I took a look at the ancestors tree of Shoes::EditBox using the shoes preview app I wrote about here, entering


Shoes.debug Shoes::EditBox.ancestors.inspect


and clicking 'Run Without App'. In the error message text box, the class hierarchy of Shoes::EditBox appeared:


[Shoes::EditBox, Shoes::Basic, Shoes::Native, Object, FileUtils, FileUtils::StreamUtils_, Kernel]


Shoes::Basic is definitely the first ancestor... but in the declaration of the EditBox class, Shoes::Native was clearly the ancestor:

(From shoes/ruby.c line 4965)

cEditBox = rb_define_class_under(cShoes, "EditBox", cNative)


What's going on? The moment of inspiration came when I looked at the definition of Shoes::Basic in lib/shoes.rb. Basic is defined as a module within the Shoes class, and then included on a large list of classes. What I'd forgotten was the way that Modules exist in the Ruby inheritance chain. When a module is included, it creates a pseudo-class known as an Iclass that sits directly above the including class in the inheritance chain, and points to the module's methods. Thus by including a module, you are changing around the ancestors of your class. A good reference can be found here, and a quick quiz to test your knowledge here.

Including modules in the display of the hierarchy might be desirable, but I'm not sure at all how to do it. By doing so, instead of having a nice tree form, suddenly we have a DAG,
which is a much more complex structure to display. Both the introspection code and display code currently expect a tree, so this is the source of the errors/lack of completeness in the current manual.

Its not clear how useful having the modules displayed in the hierarchy is, either. The Shoes::Basic module doesn't do very much, its much more useful to know which classes are descended from the Shoes::Native class.

So the simple fix, to change the manual to showing what you might expect, is to add a single line to the loop over ancestors, skipping modules.


k.ancestors[1..-1].each do |sk|
break if [Object, Kernel].include? sk
+ next unless sk.is_a? Class #don't show mixins
(tree[sk.name] ||= []) << c
c = sk.name
end


Now the introspected class hierarcy looks exactly like the hierarchy I constructed by hand. I've pushed the change to my fork of shoes on Github. Dunno when/if they'll get merged.

If someone wants to do the more complex fix of changing the structure and display to be a DAG, go for it!

P.S.

If anyone knows a way to get syntax highlighting for ruby code snippets on blogger, please let me know! Thanks!

No comments:

Post a Comment