The timeless repository

Charging Objects in Ruby

Written by Magnus Holm

Today we’ll explore a not so widely used feature in Ruby: overriding the the unary operators for fun and profit:

class CustomTimeless < Script # This script applies to every page: + url("http://timeless.judofyr.net/*") # ... except the front page: - url("http://timeless.judofyr.net/") # ... or the changelog: - url("http://timeless.judofyr.net/changelog/*") # ... and only HTML files: + type("text/html") def process(html) # Do funky thing with the HTML end end

This must be one of the coolest, yet quite unknown, technique in Ruby. For certain types of problems (e.g. when you have a set of rules) this gives you such an elegant way of describing the solution. There’s no meta programming or monkey patching involved, it’s short and sweet and best of all: it’s very intuitive.

Let’s have a look at this and a few other tricks you can do by overriding the unary operators.

You can overload the unary operators???

a = 5 b = -a c = +a d = ~a e = !a

You probably already know that it’s possible to override pretty much any operator in Ruby, so why should unary operators be treated differently? It makes very much sense when you think about it, but for some reason, most Rubyists never think about it. Unary operators just seems like a part of the language. Time for some myth busting, eh?

class Rule attr_accessor :type, :value, :charge def initialize(type, value) @type = type @value = value @charge = :neutral end def +@ @charge = :positive self end def -@ @charge = :negative self end def ~@ @charge = :neutral self end end

Woah, woah, woah. Hang on a second. +@? -@? ~@? What’s the matter with these weird method names? Let’s fire up IRB and see:

>> r = Rule.new(:url, "http://timeless.judofyr.net/*") => #<Rule:0x1003599f8 @value="http://timeless.judofyr.net/*", @charge=:neutral, @type=:url> >> r.charge => :neutral >> r.-@() >> r.charge => :negative >> +r >> r.charge => :positive >> "+(binary)".to_sym => :+ >> "+(unary)".to_sym => :+@

Like any other method, you can call them the usual way (r.-@()), but it also turns out that Ruby uses these methods internally for the unary operators:

>> a = 1 >> -a => -1 >> a.-@() => -1

So how can we (ab)use these methods?

Example: Proxy with HTML rewriting support

Once upon a time there was a Ruby proxy which worked pretty much like Greasemonkey: You could easily modify any page before it hit the browser. By using the unary operators you could create scripts and define which pages it should rewrite:

class CustomTimeless < MouseHole::Script # This script applies to every page: + url("http://timeless.judofyr.net/*") # ... except the front page: - url("http://timeless.judofyr.net/") # ... or the changelog: - url("http://timeless.judofyr.net/changelog/*") # ... and only HTML files: def process(html) # Do funky thing with the HTML end end

How does it work? Simply take the Rule class above and combine it with this:

class MouseHole::Script def self.rules @rules ||= [] end def self.url(value) rule = Rule.new(:url, value) rules << rule rule end end

Now you just need to loop through the rules to figure out if an URL matches or not. Ta-da!

Terrible Example: Negaposi

class NP def initialize a=@p=[], b=@b=[]; end def +@;@b<<1;b2c end;def-@;@b<<0;b2c end def b2c;if @b.size==8;c=0;@b.each{|b|c<<=1;c|=b};send( 'lave'.reverse,(@p.join))if c==0;@p<<c.chr;@b=[] end self end end ; begin _ = NP.new end +-+--++----+--+--+---+-------+--++--+++---+-+++-+-+-+++-----+++-_ --++-++--+--+++-++++-++-+++-+-+------+--++++-++---++-++---++-++-_ ---------+-+-----+---+--+----+----+--++- _

Who needs Ruby syntax when you have plus and minus? Negaposi gives you everything you need!

It get’s even better!

In Ruby 1.9, it gets even better: We can redefine the not operator too:

class Rule attr_accessor :priority def initialize @priority = 0 end # (Only possible in 1.9) def ! @priority += 1 self end end class Something < Script # !!! VERY IMPORTANT !!! !!! + url("http://timeless.judofyr.net/*") end

Horrible Example: MaybeNot (1.9 only)

class Object def !; rand(2).zero? end end unless ENV["USER"] == "magnus"

“Sorry, mate. I can’t reproduce the error on my machine.”

Your turn

I’d love to see what you can (ab)use this for. Please let me know if you find a suitable usage, and I’ll update this article.