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.