afkortingen voor methoden namen

Gepubliceerd op: 26.I.2006 08:42 CET
Categorieën: ruby

Commandline utilities hebben vaak korte namen; ls, rm, grep, svn etc. De subversion client svn heeft (sub-)commando’s welke allemaal afkortingen hebben; svn status kan je schrijven als svn st e.d. Toen ik een ruby wrapper class had geschreven om het svn commando, miste ik metteen de svn st variant. Natuurlijk kan je gewoon wat aliases aanmaken voor commando’s maar ik zag een kans voor een experimentje in een verloren uurtje.

Bladerend door de appendix van pickaxe 2 ben ik ooit tegen abbrev aangelopen. Gegeven een lijst termen geeft deze een hash van mogelijke afkortingen terug:

require 'abbrev'
require 'pp'
pp %w{bla die foo bar}.abbrev

levert:

{"die"=>"die",
 "bla"=>"bla",
 "d"=>"die",
 "di"=>"die",
 "foo"=>"foo",
 "f"=>"foo",
 "bar"=>"bar",
 "ba"=>"bar",
 "fo"=>"foo",
 "bl"=>"bla"}

Mooi om automagische afkortingen te leveren. We kunnen aan de slag!

Als we een methode op een object aanroepen die niet bestaat wordt de method_missing methode aangeroepen. Hier kunnen we kijken of het object met een afgekorte methode naam wordt aangeroepen en hiernaar doorspringen:

require 'abbrev'

module Shorthand
  alias :method_missing_orig :method_missing

  def method_missing(method, *args)
    original = methods.abbrev[method.to_s]
    if original
      send(original, *args)
    else
      method_missing_orig(method, *args)
    end
  end
end

En dan nu toepassen:

class String; include Shorthand; end

Maar wat heeft dit eigenlijk voor afkortingen opgeleverd? De methods methode aanroepen op een instance van String heeft geen zin omdat onze afkortingen via method_missing worden aangeroepen. Eens kijken wat abbrev oplevert, gesorteerd op lengte:

m = "".methods.abbrev
m.keys.sort{|a,b|a.size<=>b.size}.each{|k|puts "#{k.inspect} => #{m[k].inspect}"}

"%" => "%"
"b" => "between?"
"k" => "kind_of?"
">" => ">"
"<" => "<"
"z" => "zip"
"*" => "*"
"+" => "+"
"ty" => "type"
"cr" => "crypt"
"sp" => "split"
"pu" => "public_methods"
"rj" => "rjust"
"be" => "between?"
"ri" => "rindex"
"le" => "length"
"mi" => "min"
"ni" => "nil?"
"id" => "id"
"lj" => "ljust"
"[]" => "[]"
"pa" => "partition"
"is" => "is_a?"
">=" => ">="
"gr" => "grep"
..

Eigenlijk niet veel soeps. We kunnen nu "hallo wereld".le gebruiken in plaats van "hallo wereld".length, big deal.. Waarom krijgt upcase bijvoorbeeld geen mooie afkorting? Er is een upcase en upcase! daar valt dus niet aan af te korten, tenzij we deze suffix er eerst afhalen en er later weer aanplakken. Behouden we ook nog eens de punktuatie!

module Shorthand
  alias :method_missing_orig :method_missing

  def method_missing(method, *args)
    shorthands = methods.reject{|t|t =~ /[!?]$/}.abbrev
    shorthands.merge! Hash[*methods.select{|t|t =~ /!$/}.map do |t|
      t.chop
    end.abbrev.map{|a,b|[a + '!', b + '!']}.flatten]
    shorthands.merge! Hash[*methods.select{|t|t =~ /\?$/}.map do |t|
      t.chop
    end.abbrev.map{|a,b|[a + '?', b + '?']}.flatten]

    original = shorthands[method.to_s]
    if original
      send(original, *args)
    else
      method_missing_orig(method, *args)
    end
  end
end

Met als top 15 hoog rendement afkortingen:

"n" => "next"
"z" => "zip"
"ne" => "next"
"lj" => "ljust"
"sq" => "squeeze"
"n?" => "nil?"
"sp" => "split"
"sl" => "slice"
"le" => "length"
"gr" => "grep"
"st" => "strip"
"gs" => "gsub"
"rs" => "rstrip"
"m?" => "member?"
"u!" => "upcase!"

Nu hebben we zelfs u! voor upcase!, dit begint ergens op te lijken! Wat wel jammer is dat de methods methode op een object met deze module je afkortingen niet levert. Hoewel je er (bijna) niets aan hebt omdat je niet weet waar het een afkorting voor is, werkt het voor een implementatie die aliases gebruikt wel:

module Shorthand
  def self.included(klass)
    methods = klass.public_instance_methods
    shorthands = methods.reject{|t|t =~ /[!?]$/}.abbrev
    shorthands.merge! Hash[*methods.select{|t|t =~ /!$/}.map do |t|
      t.chop
    end.abbrev.map{|a,b|[a + '!', b + '!']}.flatten]
    shorthands.merge! Hash[*methods.select{|t|t =~ /\?$/}.map do |t|
      t.chop
    end.abbrev.map{|a,b|[a + '?', b + '?']}.flatten]

    shorthands.each{|k,v| klass.class_eval "alias #{k} #{v}"}
  end
end

Het was een leuk experimentje in een verloren uurtje maar de afkortingen waar abbrev meekomt zijn voor m’n Svn class waardeloos. De commit methode zou afgekort worden naar co wat voor de commandline tool gelijk is aan checkout, niet echt wenselijk. Daarbij kan je je afvragen of een beter afkort algoritme veel uit zal halen. Dergelijk automagische afkortingen zullen zeer instabiele interfaces opleveren. De kans is te groot dat de introductie van een nieuwe methode een belangrijk deel van de afkortingen die je ingebruik hebt omzeep zal helpen.

Geen goed idee, ik maak wel aliases met de hand, wel een interessant uurtje waarin ik anders op een collega’s had zitten wachten.