The best kittens, technology, and video games blog in the world.

Friday, November 03, 2006

magic/help for Ruby

First steps by fofurasfelinas from flickr (CC-NC-ND) Help is a weakness of almost all programming languages. Ruby help really sucks too. For example let's try to get help on sync method of an opened File:

$ irb
irb(main):001:0> f ="/dev/null")
=> #<File:/dev/null>
irb(main):002:0> help f.sync
------------------------------------------------ REXML::Functions::false
     REXML::Functions::false( )
=> nil
irb(main):003:0> help 'f.sync'
Bad argument: f.sync
=> nil
irb(main):004:0> help File.sync
NoMethodError: undefined method `sync' for File:Class
        from (irb):4
irb(main):005:0> help 'File.sync'
Nothing known about File.sync
=> nil
irb(main):006:0> help 'File#sync'
Nothing known about File#sync
=> nil
irb(main):007:0> help 'sync'
More than one method matched your request. You can refine
your search by asking for information on one of:

     IO#sync, IO#fsync, IO#sync=, Zlib::GzipFile#sync,
     Zlib::GzipFile#sync=, Zlib::Inflate#sync,
     Zlib::Inflate#sync_point?, Mutex#synchronize,
     MonitorMixin#mon_synchronize, MonitorMixin#synchronize,
     StringIO#sync, StringIO#fsync, StringIO#sync=
=> nil
irb(main):008:0> eat flaming death
(irb):8: warning: parenthesize argument(s) for future version
NameError: undefined local variable or method `death' for main:Object
        from (irb):8
irb(main):009:0> ^D
$ firefox
Of course nobody would actually do that. Everyone either visits Google the first thing, or talks with objects using reflection. The help system is just way too weak. It's not that the help isn't there, Ruby has plenty of documentation. It's just too hard to find. Compare Ruby:

help "Array#reverse"
---------------------------------------------------------- Array#reverse
     array.reverse -> an_array
     Returns a new array containing _self_'s elements in reverse order.

        [ "a", "b", "c" ].reverse   #=> ["c", "b", "a"]
        [ 1 ].reverse               #=> [1]
with Python:

>>> help([].reverse)
Help on built-in function reverse:

    L.reverse() -- reverse *IN PLACE*
So Ruby has more documentation, but it's more difficult to access it. At least it was till today morning. Because right now, Ruby totally dominates ! If you pass class, class name, or class instance, you get documentation on the class:
irb(main):001:0> help "Array"
irb(main):002:0> help Array
irb(main):003:0> help { Array }
irb(main):004:0> help { ["a", "b", "c"] }
----------------------------------------------------------- Class: Array
     Arrays are ordered, integer-indexed collections of any object.
     Array indexing starts at 0, as in C or Java. A negative index is
     assumed to be relative to the end of the array---that is, an index
     of -1 indicates the last element of the array, -2 is the next to
     last element in the array, and so on.


     Enumerable(all?, any?, collect, detect, each_cons, each_slice,
     each_with_index, entries, enum_cons, enum_slice, enum_with_index,
     find, find_all, grep, include?, inject, map, max, member?, min,
     partition, reject, select, sort, sort_by, to_a, to_set, zip)

Class methods:
     [], new

Instance methods:
     &, *, +, -, <<, <=>, ==, [], []=, abbrev, assoc, at, clear,
     collect, collect!, compact, compact!, concat, dclone, delete,
     delete_at, delete_if, each, each_index, empty?, eql?, fetch, fill,
     first, flatten, flatten!, frozen?, hash, include?, index, indexes,
     indices, initialize_copy, insert, inspect, join, last, length, map,
     map!, nitems, pack, pop, pretty_print, pretty_print_cycle, push,
     rassoc, reject, reject!, replace, reverse, reverse!, reverse_each,
     rindex, select, shift, size, slice, slice!, sort, sort!, to_a,
     to_ary, to_s, transpose, uniq, uniq!, unshift, values_at, zip, |
If you call a method inside the block, you get documentation on it. It won't really be called, because magic/help plugs into debugging hooks (set_trace_func). So you can safely ask for help on start_global_thermonuclear_warfare.
irb(main):005:0> help { 2 + 2 }
--------------------------------------------------------------- Fixnum#+
     fix + numeric   =>  numeric_result
     Performs addition: the class of the resulting object depends on the
     class of +numeric+ and on the magnitude of the result.
It doesn't matter whether it's in the class, or one of its ancestors, or an included module. You can also pass Method or UnboundMethod object, or method name. It all does the right thing.
irb(main):006:0> f ="/dev/null")
=> #<File:/dev/null>
irb(main):007:0> help { f.sync }
irb(main):008:0> help "File#sync"
irb(main):009:0> help f.method(:sync)
irb(main):010:0> help File.instance_method(:sync)
---------------------------------------------------------------- IO#sync
     ios.sync    => true or false
     Returns the current ``sync mode'' of _ios_. When sync mode is true,
     all output is immediately flushed to the underlying operating
     system and is not buffered by Ruby internally. See also +IO#fsync+.

        f ="testfile")
        f.sync   #=> false
magic/help tries to guess whether you meant class or instance method. So help "Dir.[]" gives you documentation for class method of Dir, while help "Array.[]" gives you documentation for instance method of Array. Using magic/help requires almost no effort. Simply copy magic_help.rb to some visible place, and add require 'magic_help' to your ~/.irbrc. Works with either 1.8 or 1.9. I haven't converted it to a gem yet. For now go to magic/help website and get a tarball or a zip file. Documentation is minimal, as it was just finished. Unit test coverage is naturally 100%.


Anonymous said...

Actually, I find that Ruby doesn't have much more documentation than Python (since that's one of your comparisons).

The huge difference is that RI often has examples, which the Python doc never has. And a little introduction to the class for classes, too.

I find Python's help to be more useful overall. One of the features I do like best in Python's help, which RI plain and simply doesn't have, is that Python's help is somewhat recursive: call `help` on any Python class and you'll get a list of all the class' methods and the help blub for each of these methods.

In Ruby, you only get a huge block of unreadable text dumping all of the methods, which is barely more useful than just printing `object.methods`.

taw said...

Anonymous: Changing Ruby to output method documentation together with class documentation is trivial. In fact I tried such changes when I was coding magic/help, but eventually decided to publish the simplest code that only changes the way documentation is searched, not how it is displayed.

The obvious problem is size - Ruby descriptions are much longer, with examples and multiline descriptions, while in Python methods are usually described by a header line + 1, rarely 2 description lines. And many things are methods in Ruby and global functions in Python.

Just a quick test.

Documentation for list in Python: 123 lines.

Documentation for all instance methods of Array in Ruby: 1147 lines.
Only counting methods defined in Array class (not Object/Enumerable) - 839 lines.

It isn't really reasonable to display that much.

Anonymous said...

Hi Tomasz,

This is really great! Thanks!!!

One little thing, your tests fail on Windows because"/dev/null") fails. Changing this to"tc_magic_help.rb") works.

Wayne Vucenic

taw said...

Anonymous: Thanks for reporting the problem. I changed it to open __FILE__ instead.

Logan Capaldo said...

I'm pretty sure I speak for everyone when I say "This _needs_ to be in the standard distribution" if not necessarily require'd by default.

taw said...

Logan Capaldo: magic/help is very small and very compatible. Either it or something with similar functionality should definitely be enabled by default in the standard distribution.

Before I submit a patch I'd like to check how Do-What-I-Mean it really is. The test suite covers only the simplest cases and it is very likely that magic/help doesn't handle many complex situations "right" (especially since "right" is rather subjective here).

I would be great if you (and other people who use magic/help) sent me extra test cases documenting your expectations. They are valuable no matter whether magic/help currently agrees or disagrees.

It is difficult to fairly cover all styles of Ruby programming, and I would like to avoid biasing it for my style.

Anonymous said...

Why won't this work? I am beginning. If possible, fixthe errors and give me a error-free one.
puts "What is the price for an extra large pizza?"
exprice = gets
puts "What is the price for a large pizza?"
largeprice = gets
puts "What is the price for a medium pizza?"
medprice = gets
puts "What is the price for a small pizza?"
smallprice = gets
puts "What is the price per topping?"
toppinglz = gets
puts "What is the price for a soda or drink?"
sodaprice = gets
puts "What is the name of the speciality dessert or side dish?"
side = gets.chomp
puts "What is the price of your side?" + side.to_s end
sideprice = gets
puts "Press ENTER to get the order!"
enter = gets
puts "Type of pizza."
type = gets.chomp
puts "Amount of toppings."
amount = gets.to_i
if amount = 3
puts "Topping 1."
toppingone = gets.chomp.to_s
puts "Topping 2."
toppingtwo = gets.chomp.to_s
puts "Topping 3."
toppingthree = gets.chomp.to_s
topping = 3
toppings = toppingone + " " + toppingtwo " " + toppingthree
elsif amount = 2
puts "Topping 1."
toppingone = gets.chomp.to_s
puts "Topping 2."
toppingtwo = gets.chomp.to_s
topping = 2
toppings = toppingone + " " + toppingtwo "
elsif amount = 1
puts "Topping 1."
toppingone = gets.chomp.to_s
topping = 1
toppings = toppingone
puts "ERROR in amount of toppings. Cannot exceed 3."
puts "If the size is ex-large, press 1. Large, press 2. Medium, press 3. Small, press 4."
sizef = gets
if sizef = 1
price = exprice
type = "ex-large"
elsif sizef = 2
price = largeprice
type = "large"
elsif sizef = 3
price = medprice
type = "medium"
price = smallprice
type "small"
puts "Press 5 if you want a drink. If not, press anything else."
drink = gets
if drink = 5
sodapricef = sodaprice
drinkyes = "With a Drink"
sodapricef = 0
drinkyes = "without a Drink"
puts "Do you want a " + side + "? If you do press 9. If not press anyting else."
sidel = gets
if sidel = 9
sidef = sideprice
sidel = "With a " + side
sidef = 0
sidel = "Without a " + side
puts "Your order is a:"
puts topping.chomp.to_s + " topping pizza."
puts toppings
puts type.chomp.to_s
puts drinkyes.chomp.to_s
puts sidel.chomp.to_s
puts "Total Cost:"
puts "$"sidef.to_f + sodapricef.to_f + price.to_i + topping.to_f * toppinglz.to_f