Irb is a Text Adventure

December 16th, 2009

Ruby's flavor of classed OO offers a thoughtful balance of rigidity and flexibility along with plenty of introspection and reflection. Over time I have come to experience this model less as a framework for thought and more as a living thing with which to converse. Dave Thomas brought up the prospect of a Ruby object browser over nine years ago. That thread raised questions that still haven't been answered.

Unfortunately for a browser the rigidity offered by Ruby is optional. I personally feel that HTML with links is the only graphical form that comes close to capturing the needed range. I just described a blank slate for a digraph so feel free to think it's lame. The object browser is possible and desirable but after all these years Irb is still the Ruby browser of choice.

My take is that every object in Irb is like a noun in a text adventure, and likewise every object is different. There are common verbs and uncommon verbs, and not every noun supports every verb. The components of trial, error, failure, and reward are the same. Nothing ruins my suspension of disbelief like having to consult documentation, and I like to believe that I am programming for as long as possible. The notable missing link to the underground world of Zork is "look". Here is "look". It is nothing more than a mashup of introspection methods but I've found it indispensable.


# Examples:
#   foo.look
#   foo.class.look
#   foo.method(:meth).look
#   foo.method(:meth).source_context
#   foo.instance_variable_hash

Object.class_eval do
  def instance_variable_hash
    ret = {}
    instance_variables.each { |v|
      ret[v] = instance_variable_get(v)
    }
    ret
  end

  def look
    {
      :class => self.class,
      :interesting_methods => (methods - Object.new.methods).sort,
      :instance_variables => instance_variables.sort,
    }
  end
end

Class.class_eval do
  def look
    {
      :class => self.class,
      :ancestors => ancestors,
      :included_modules => included_modules,
      :interesting_methods => methods - Class.new.methods,
      :instance_methods => instance_methods(false),
      :class_variables => class_variables,
      :instance_variables => instance_variables,
      :constants => constants,
      :source_files => methods(false).collect { |m| method(m).source_location.try(:first) }.compact.sort.uniq,
    }
  end
end

Module.class_eval do
  def look
    {
      :class => self.class,
      :ancestors => ancestors,
      :included_modules => included_modules,
      :class_variables => class_variables,
      :interesting_methods => methods - Class.new.methods,
      :instance_methods => instance_methods(false),
      :instance_variables => instance_variables,
      :constants => constants,
    }
  end
end

Method.class_eval do
  if RUBY_VERSION < "1.9"
    def source_location
    end
  end

  def look
    {
      :class => self.class,
      :arity => arity,
      :name => name,
      :owner => owner,
      :receiver => receiver,
      :source_location => source_location,
    }
  end

  def source_context
    return nil unless source_location
    line_number = source_location.second
    file = File.open(source_location.first, "r")
    lines = file.lines.drop(line_number - 1)

    indent = lines.first.scan(/^ */).first.length
    end_marker = Regexp.new("^" << " " * indent << "end")

    interesting_lines = []
    begin
      line = lines.shift
      interesting_lines << line
    end until line =~ end_marker or interesting_lines.count > 20 or line.nil?

    file.close

    source_location << interesting_lines.join
  end
end

Plays well with 1. Wirble, 2. pretty printing, 3. Ruby 1.8, 4. Ruby 1.9, and OF COURSE your .irbrc.


Your Response