Method Auto Completion (#110)

by Robert Dober

Command Line Interfaces very often support command abbreviations The purpose of this quiz is to automatically dispatch to methods on unambiguous abbreviations, ask the user for clarification in case of ambiguous abbreviations and raise a NoMethodError in case of an abbreviation that cannot be matched to any command.

Behavior of other methods defined in a class shall not be altered.

Be creative about the interface and about behavior. I have OTOH defined a small test suite that makes assumptions about the interface and behavior. But the test suite is only there for your convenience.

What is said below applies to the test suite and shall in no way inhibit any alternative ideas.

ruby
class Mine
abbrev :step, :next, :stop
abbrev :exit
end

Mine.new.e # should resolve to exit
Mine.new.st # should prompt the user
Mine.new.a # should still raise a NoMethodError

Abbreviation targets themselves are not expanded.

ruby
class Nine
abbrev :hash
abbrev :has
end

Nine.new.ha # => [:hash, :has]
Nine.new.has # => NoMethodError

class Nine
def has; 42; end
end
Nine.new.has # => 42

In order to allow for automated testing the test code shall not prompt the user in case of an ambiguous abbreviation but return an array containing all (and only all) possible completions as symbols. Note that the test suite sets the global variable $TESTING to a true value for your convenience.

Test Suite


Quiz Summary

by Robert Dober

First of all I would like to thank all submitters.

One of the best features of the Ruby Quiz is it tolerance brings up new ideas so often.

In order to honor this feature I will first present Daniel Finnie's solution. It just abbreviates all defined methods. This very useful a feature which I will demonstrate after discussion of the code.

ruby
class Object
    def method_missing(method, *args, &blk)
        # Gather all possible methods it could be.
        possibleMethods = self.methods.select {|x|
x =~ /^#{Regexp.escape(method.to_s)}/
}
       
        case possibleMethods.size
        # No matching method.
        when 0
            raise NoMethodError.new(
"undefined method `#{method}' for " +
"#{self.inspect}:#{self.class.name}"
)
       
        # One matching method, call it.
        when 1
            method = possibleMethods.first
            send(method, *args, &blk)
       
        # Multiple possibilities, return an array of the possibilities.
        else
            "Ambigous abbreviation #{method} ->  " +
"#{ possibleMethods.join(", ")}"
        end
    end
end

Nothing very complicated there, just define method_missing in Object as a catch all for undefined methods in any object. Than he creates an array of all methods which are legal completions of a potential abbreviation. In case there are none an original NoMethodError is mimicked. In case there is exactly one it is executed via send, please note the (method, *args, &blk) syntax, the &blk part has been forgotten in some solutions but is vital in #method_missing. And eventually if there are more Daniel returned an array of all completions for testing. I will discuss this later.

I have adapted this to return a message explaining what completions exist for the abbreviation. Why? Well I just fired up my irb and requested Daniel's solution, see what I got, much less typing for free. Here are some excerpts of my irb session.

irb(main):001:0> require 'sol2'
=> true
irb(main):002:0> "a".le
=> 1
irb(main):003:0> "abba".le
=> 4
irb(main):004:0> "Hello World".spl
=> ["Hello", "World"]
irb(main):005:0> "Hi there Daniel".sp
=> ["Hi", "there", "Daniel"]
irb(main):006:0> "Hi there Daniel".s
=> "Ambigous abbreviation s ->  select, slice, sub!, squeeze, send, split,
size, strip, succ!, squeeze!, sub, slice!, scan, sort, swapcase, swapcase!,
sum, singleton_methods, succ, sort_by, strip!"
irb(main):007:0> 12.x
NoMethodError: undefined method `x' for 12:Fixnum
                from ./sol2.rb:9:in `method_missing'
                from (irb):7

Quite nice as a side product, no?

Let us turn towards solutions which respected the idea of defining certain abbreviations, being interpreted in Command Line Interfaces for example. It was extremely difficult to chose a solution because most of the solutions had their strong parts. I eventually decided to comment on Donald Ball's solution as it was maybe the most readable solution for myself. I had discussed this with James who favored other solutions for other reasons and I will mention these reasons shortly. As always there is something in every solution so take your time and read them.

Ok here is Donald's code - slightly modified again.

ruby
require 'set'

module AutoComplete
    module ClassMethods
        attr_reader :abbrs
        def abbrev(*args)
            # TODO abbrs might be better implemented as a sorted set
            @abbrs ||= Set.new
            @abbrs += args
        end
    end
    module ObjectMethods
        def method_missing(id, *args, &blk)
            # if it is an exact match, there is no corresponding method
# or else it would have been called
            if self.class.abbrs.include?(id)
                super
            end
            s = id.to_s
            len = s.length
            # find all abbreviations which begin with id and have
# active methods
            matches = self.class.abbrs.select { |abbr|
abbr.to_s[0,len] == s && respond_to?(abbr)
}
            if matches.length == 0
                super
            elsif matches.length == 1
                send(matches[0], *args, &blk)
            else
                matches
            end
        end
    end
end

class Object
    extend AutoComplete::ClassMethods
    include AutoComplete::ObjectMethods
end

Although this can be done more concisely I eventually started to like the explicit way.

Lots of little details can be changed and they were in other solutions. Not everybody wanted a NoMethodError thrown in case an abbreviation target was an abbreviation of a different method and was not there.

Ken Bloom was the first to point out that it was a bad idea to return an array of abbreviations in case of ambiguities. I completely agree, that was only for the test code anyway.

A great majority of the solutions follow this idea which is certainly a must under some circumstances. I imagine a DSL which is not interactive and where the interpreter has no choice than to throw an Exception, good thinking here.

Ken's solution has three features noteworthy, first he added the list of possible completions as an attribute of the exception he throws, secondly he just used the abbrev Standard Library Module and thirdly he used #super in #method_missing to raise a NoMethodError. super in that case calls Kernel#method_missing. This took me some time to figure out. I guess the road to Ruby mastery is not answering quizzes but submitting quizzes and understanding the solutions. But you will find some of these features in other solutions too.

Let us finish the summary with his code, another astonishing example of how much can be done with so little code in Ruby.

ruby
require 'abbrev'

class AmbiguousExpansionError < StandardError
attr_accessor :candidates
def initialize(name,possible_methods)
super("Ambiguous abbreviaton: #{name}\n"+
"Candidates: #{possible_methods.join(", ")}")
@candidates=possible_methods
end
end

module Abbreviator
def method_missing name,*args
abbrevs=methods.abbrev
return send(abbrevs[name.to_s],*args) if abbrevs[name.to_s]
meths=abbrevs.reject{|key,value| key!~/^#{name}/}.values.uniq
raise AmbiguousExpansionError.new(name, meths) if meths.length>1
return super(name,*args)
end
end

Many thanx to everyone....